Redux and React

How to use Redux to manage state in a React Application

Intro

Recently, I completed my build of a full-stack application using React on the front end and rails on the backend. This application had the most components out of all of my projects....21 (and counting). In my past applications, I used React state and props to handle state. In these applications, I made sure to add state in the appropriate parent component. I passed it for miles, through props, to the smallest child component. Then, I realized I needed to pass objects from the child component, back to the parent, so off I would go creating a callback function and passing it down the component tree all over again. This process became old, and I was thankful to find Redux (a global state management library) to avoid this endless process. I am far from an expert, but I would like to relay how I used redux in hopes it might help you with your next application.

What does Redux allow you to do?

Redux allows the storing of state in a "store." Unlike React state, which is only accessible to children components by way of passing props from their parent components, the store is accessible by any component, at any time. Every component can update state. Greatest of all, Redux eliminates the need to remember which components state was passed through since it is always available to each component.

Redux does not have to eliminate all props, or local state, but it certainly makes global state much easier to handle.

Getting Started:

Install Redux

After setting up a react application, we will need to install redux and react-redux. We can do this with npm by using the following command: $ npm install redux react-redux.

Install Middleware Thunk

Thunk middleware for Redux allows us to write, "functions with logic inside that can interact with a Redux store's dispatch and getState methods." This will help us out in our process. To install this with npm, you can use the following command $ npm install redux-thunk.

Set Up

In my react app, I always add a .src file. This file contains all my components, and it is the perfect place to build out our redux items. In the .src file, we first need to create two folders. To keep these straight, title the first folder reducers and the second actions.

Reducers Folder

In order to have a physical store (like Walmart), you must have products inside for customers to purchase. In Redux, reducers are what will make up our store. They are the items that will contain and manipulate our state so we can view it on the front end. They give us the ability to move complicated functions from our components to one place where we can handle all requests.

Set Up a Reducer

In the reducers folder create a file. It is easiest to name the file with the title of your state, followed by the word "Reducer." This file will be a .js file. In my case, I am going to create state for a user who has logged into my react app. I titled my file userReducer.js.

In this file, we are going to set up a function to return state based on what our component calls for. I will explain this in a little more detail shortly, but let me start by showing you what my a basic outline looks like:

const initialState = null

const userReducer = (state=initialState, action) => {
  switch(action.type) {
    //If there are no other cases, we will just return state which is curently null. 
    default:
      return state;
  }

}

export default userReducer;

I know this might look crazy, but hang in there. Let me go over some basics. This reducer takes in an action that has a type and a payload. We will form our action later, but currently, you can see action is the second item our function takes in. The first is state. Just like when we use useState to set a default state in React components, we can set a default state (like initialState in the example above) and assign it to our state. In the case above, we have set our reducer to always return null if we do not add any other cases to it.

The switch statement above will return a case based on the action.type we pass to it. So, let's add some case statements to our switch to help clarify what we would like the reducer to return:

const initialState = null

const userReducer = (state=initialState, action) => {
  switch(action.type) {
    //If a user signs in, we want to return the user as state. 
    case "ADD_USER":
      return [action.payload]
    //If a user signs out, we want to delete the user information from state, and return our initial state of null. 
    case "DELETE_USER":
      state = initialState;
      return state
    default:
      return state;
  }
}

export default userReducer;

In our reducer, we now have a case statement allowing us to add and delete a user. As you might notice, we can use simple JSX and javascript methods to change state based on what we are doing in our component. The payload (action.payload), which is coming through our action, is the data returned from our fetch request in the component.

Now that we have a simple reducer, let's set up an action so we can pass objects to our state. Make sure to remember the case statements declared in the reducer, so we can use them in the action. In the example above, we will remember ADD_USER and DELETE_USER.

Actions

Actions are what allow us to connect our reducer to the component. They are what we create to pass data to state from a component.

Setting Up an Action

Next, we need to create a file in the actions folder. It is simple to title this file with the title of the state we are trying to manipulate. This file will be a .js file also. For example, I named my action folder user.js.

In this file, we need to export functions for each of the cases we have in our reducers folder. This will connect the reducer with our component and allow us to update state in the reducer. It might make more sense to describe this as the setState function we use in a component. Before I explain further, let me show you what this looks like:

export const addUser = (user) => {
  return {
    //Our type must match the case statement from the reducer file. 
    type: "ADD_USER",
    //The payload is passed to this function by our component calling this function. 
    payload: user
  }
}

export const deleteUser = (user) => {
  return {
    //Our type must match the case statement from the reducer file.
    type: "DELETE_USER",
    payload: user
  }
}

In the action above, you can see we have an action type defined for each case in the reducer. For example, in the addUser function, we are sending the ADD_USER case a payload of user. user is the data we are sending from our React component, and we will see this in action in just a second. In order to pass user to our state, we will import the addUser function to the component, and send (or dispatch) our user data to the reducer where state is held.

Now that we have these folders set up, let's connect our state to the front end.

Set Up an Index.js File

Your application likely already has an index.js file in the .src folder. We want to create a second one that is different from the first. You can create this file in the reducer folder and title it index.js. The reason we need to create this folder is to combine all of our reducers into the store. Without this folder, you can only have one reducer in the store, and likely this will not work for most applications that need to maintain state for multiple areas of the application.

In the index.js folder, we need to start by importing { combineReduces } from 'redux'. After, we need to import each of our reducers. I am going to add more reducers to demonstrate this idea:

import { combineReducers } from "redux";
import userReducer from "./userReducer";
import incidentReducer from "./incidentReducer";
import personnelReducer from "./personnelReducer";
import resourceReducer from "./resourceReducer";

Next, we need to combine all the reducers like so:

import { combineReducers } from "redux";
import userReducer from "./userReducer";
import incidentReducer from "./incidentReducer";
import personnelReducer from "./personnelReducer";
import resourceReducer from "./resourceReducer";

export default combineReducers({
  user: userReducer, //store.user when using the selector
  incident: incidentReducer, //store.incident
  personnel: personnelReducer, //store.personnel
  resources: resourceReducer //store.resources 
})

That's it for this folder. Make sure to include all your reducers so you can access them on the front end!

Configure the True Index.js

Now, we need to do some configuring inside the index.js file contained in the .src folder. (Different from the one we just created) This would be your normal index.js file.

import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux'; //!!!!createStore is no longer the recommended way to handle state. A strikethrough will strike through this, but it will still work. 
import { composeWithDevTools } from '@redux-devtools/extension'; // We can use a chrome extension to provide redux dev tools
import thunk from 'redux-thunk'; //Thunk middleware
import rootReducer from './reducers'; //This is the combination of reducers we created in the other index.js folder

Once we import the items described above we can implement them by creating a variable for the store, and wrapping our app component in the <Provider>. Lastly, we need to pass our store variable as a prop to the provider.

This will look something like this:

import React from 'react';
import { composeWithDevTools } from '@redux-devtools/extension';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './reducers';
import thunk from 'redux-thunk';
import 'materialize-css/dist/css/materialize.min.css';
import { BrowserRouter } from 'react-router-dom';
import ReactDOM from 'react-dom/client';
import App from './components/App';
import reportWebVitals from './reportWebVitals';

//Creating a variable for the store
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store={ store }> 
        <App />
      </Provider>
    </BrowserRouter>
  </React.StrictMode>
);

reportWebVitals();

Connect With a Component

This has been a lot of work, but we are super close, so hang in there. Last, but not least, we need to connect our state with our components. We will need two more items to do this.

Dispatch

Dispatch is how we send data through the action to the reducer in order to update state. Always remember, we dispatch data to the store. We select state (more on this in a second) to retrieve state.

First, find a component you would like to dispatch data from. In this component:

import { useDispatch } from 'react-redux';

Now set useDispatch to a variable:

const dispatch = useDispatch();

Lastly, you will need to import the function you set up in the reducer for the task you would like to complete. In my example, I wanted to fetch a user's information if they were currently logged in, and assign their information to the user state. I first import the addUser action we set up earlier, then dispatch the user data to the reducer through this function. I know this might be a bit confusing, but it looks like this:

import { useDispatch } from 'react-redux';
import { addUser } from "../actions/user";

function App() {
  const dispatch = useDispatch();

  useEffect(() => {
    fetch('/me').then((r) => {
      if(r.ok){
        r.json().then(user => dispatch(addUser(user)))//I am dispatching the addUser action to the reducer where the reducer will update state with the users information. 
      }
    });
  }, []);

export default App;

To delete the user, I can simply dispatch(deleteUser(user))to the reducer, which will reset my state.

Select

Now that we have updated our state, we need to access it. We will do this with a selector. The setup process is very similar to setting up dispatch. You can access state in any component by following these steps. State can still be passed down to children props through normal props. For instance, if you have nested data, you may want to use useSelectto grab state and .map nested objects to a different component.

To get state, you will first need to import useSelect into your component:

import { useSelect } from 'react-redux';

Then, we can assign a variable to select the state we would like to retrieve. Remember, if we have multiple reducers, we need to grab state from the appropriate reducer by using store.ourStateReducer. This looks like this:

const user = useSelector((store) => (store.user)); //Returns our user state.

Now, we are able to use the user information to set up our app however we might like. Altogether, select and dispatch might look like this:

import { useDispatch, useSelector } from 'react-redux';
import { addUser } from "../actions/user";

function App() {
  const dispatch = useDispatch();
  const user = useSelector((store) => (store.user));

  useEffect(() => {
    fetch('/me').then((r) => {
      if(r.ok){
        r.json().then(user => dispatch(addUser(user)))
      }
    });
  }, []);

  return (
    <>
      <div>
        <Navbar />
      </div>
      <div className="container">
        {user ? (
          <Usercard props={user}/>
        ):(
          <SignIn />
        )}
      </div>
    </>
  );
}


export default App;

Conclusion

Redux makes handling state much easier. Initially, it can be a little challenging to grasp, but after discovering the flow of data, Redux will become a simple tool of great help to you. Never forget, you can still always handle simple state inside a component, but Redux can handle the state you need multiple components to have access to. I have created a short list of all the steps above should it be helpful. I do not claim to be an expert, but hopefully this will get you started.

Review of the Steps:

  1. Install Redux - $ npm install redux react-redux

  2. Install Thunk - $ npm install redux-thunk

  3. Create reducer and action folders in the .src folder.

  4. Set up the reducer:

    1. Create a default state.

    2. Create a case to update state in the way you would like.

  5. Set up the action:

    1. Assign the type to the case in the reducer.

    2. Assign the payload to the payload you will be passing the reducer.

  6. Create an index.js file in the reducers folder and set up this file:

    1. import { combineReducers } from "redux";

    2. Import all the reducers you have created to this folder.

    3. Combine them together.

  7. Configure the normal index.js file in .src to work with Redux:

    1. Import the appropriate tools from redux and thunk (see above).

    2. Create a variable for the store.

    3. Wrap the <App/> component in the Provider component and pass the store variable as a prop.

  8. Depending on if you are dispatching or selecting state:

    1. Dispatch (updating state):

      1. import { useDispatch } from 'react-redux';

      2. Assign useDistpach() to a variable.

      3. Import the function you would like to use from the action.

      4. Dispatch the action with the data(payload) to the reducer.

    2. Select (accessing state):

      1. import { useSelector } from 'react-redux';

      2. Assign a variable to get the appropriate state.

      3. Use the variable as state wherever you might like.

Bonus

A cool thing about actions, is that you can use them to handle tasks you might repeat in various components. For example, maybe you want various components to fetch data when they render. You can create an action that handles this for you and updates state so you do not have to type the fetch multiple times in multiple components.

Here is an example of my set up, and how I used this in the component upon the pages render:

Action:

export const loadIncidents = () => {
  return (dispatch) => {
  fetch('/incidents')
  .then((r) => r.json())
  .then((incidents) => dispatch({type: "LOAD_INCIDENTS", payload: incidents}))
  }
}

Reducer:

const initialState = []

const incidentReducer = (state=initialState, action) => {
  switch(action.type) {
    case "LOAD_INCIDENTS":
        return action.payload
    default:
      return state;
    }
}

export default incidentReducer;

Component:

import { loadIncidents } from "../actions/incidents";
import { useDispatch } from "react-redux";

const Home = () => {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(loadIncidents());
    dispatch(loadResources())
  }, []);

Did you find this article valuable?

Support Jerry Fitzner by becoming a sponsor. Any amount is appreciated!