Photo by Lautaro Andreani on Unsplash
Redux and React
How to use Redux to manage state in a React Application
Table of contents
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 useSelect
to 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:
Install Redux -
$ npm install redux react-redux
Install Thunk -
$ npm install redux-thunk
Create reducer and action folders in the .src folder.
Set up the reducer:
Create a default state.
Create a
case
to updatestate
in the way you would like.
Set up the action:
Assign the type to the
case
in the reducer.Assign the payload to the payload you will be passing the reducer.
Create an
index.js
file in thereducers
folder and set up this file:import { combineReducers } from "redux";
Import all the reducers you have created to this folder.
Combine them together.
Configure the normal
index.js
file in.src
to work with Redux:Import the appropriate tools from redux and thunk (see above).
Create a variable for the store.
Wrap the
<App/>
component in theProvider
component and pass thestore
variable as a prop.
Depending on if you are dispatching or selecting state:
Dispatch (updating state):
import { useDispatch } from 'react-redux';
Assign
useDistpach()
to a variable.Import the function you would like to use from the action.
Dispatch the action with the data(payload) to the reducer.
Select (accessing state):
import { useSelector } from 'react-redux';
Assign a variable to get the appropriate state.
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())
}, []);