Redux is a state management tool, built specifically for client-side JavaScript applications that depend heavily on complex data and external APIs, and provides great developer tools that make it easy to work with your data.
What Does Redux Do?
Simply put, Redux is a centralized data store. All of your application data is stored in one large object. The Redux Devtools make this easy to visualize:
This state is immutable, which is a strange concept at first, but makes sense for a few reasons. If you want to modify the state, you have to send out an action, which basically takes a few arguments, forms a payload, and sends it over to Redux. Redux passes the current state to a reducer function, which modifies the existing state and returns a new state that replaces the current one and triggers a reload of the affected components. For example, you might have a reducer to add a new item to a list, or remove or edit one that already exists.
Doing it this way means you’ll never get any undefined behavior with your app-modifying state at will. Also, because there’s a record of each action, and what it changed, it enables time-travel debugging, where you can scroll your application state back to debug what happens with each action (much like a git history).
Redux can be used with any frontend framework, but it’s commonly used with React, and that’s what we’ll focus on here. Under the hood, Redux uses React’s Context API, which works similarly to Redux and is good for simple apps if you want to forego Redux altogether. However, Redux’s Devtools are fantastic when working with complex data, and it’s actually more optimized to prevent unnecessary rerenders.
If you’re using TypeScript, things are a lot more complicated to get Redux strictly typed. You’ll want to follow this guide instead, which uses typesafe-actions to handle the actions and reducers in a type-friendly manner.
Structuring Your Project
First, you’ll want to lay out your folder structure. This is up to you and your team’s styling preferences, but there are basically two main patterns most Redux projects use. The first is simply splitting each type of file (action, reducer, middleware, side-effect) into its own folder, like so:
This isn’t the best though, as you’ll often need both an action and reducer file for each feature you add. It’s better to merge the actions and reducers folders, and split them up by feature. This way, each action and corresponding reducer are in the same file. You
This cleans up the imports, as now you can import both the actions and reducers in the same statement using:
It’s up to you whether you want to keep Redux code in its own folder (/store in the above examples), or integrate it into your app’s root src folder. If you’re already separating code per component, and are writing a lot of custom actions and reducers for each component, you might want to merge the /features/ and /components/ folders, and store JSX components alongside reducer code.
If you’re using Redux with TypeScript, you can add an additional file in each feature folder to define your types.
Installing and Configuring Redux
Install Redux and React-Redux from NPM:
You’ll also probably want redux-devtools:
The first thing you’ll want to create is your store. Save this as /store/index.js
Of course, your store will get more complicated than this as you add things like side-effect addons, middleware, and other utilities like connected-react-router, but this is all that’s required for now. This file takes the root reducer, and calls createStore() using it, which is exported for the app to use.
Next up, we’ll create a simple to-do list feature. You’ll probably want to start by defining the actions this feature requires, and the arguments that are passed to them. Create a /features/todos/ folder, and save the following as types.js:
This defines a few string constants for the action names. Regardless of the data you’re passing around, each action will have a type property, which is a unique string that identifies the action.
You aren’t required to have a type file like this, as you can just type out the string name of the action, but it’s better for interoperability to do it this way. For example, you could have todos.ADD and reminders.ADD in the same app, which saves you the hassle of typing _TODO or _REMINDER every time you reference an action for that feature.
Next, save the following as /store/features/todos/actions.js:
This defines a few actions using the types from the string constants, laying out the arguments and payload creation for each one. These don’t have to be entirely static, as they are functions—one example that you might use is setting a runtime CUID for certain actions.
The most complicated bit of code, and where you’ll implement most of your business logic, is in the reducers. These can take many forms, but the most commonly used setup is with a switch statement that handles each case based on the action type. Save this as reducer.js:
The state is passed as an argument, and each case returns a modified version of the state. In this example, ADD_TODO appends a new item to the state (with a new ID each time), DELETE_TODO removes all items with the given ID, and EDIT_TODO maps and replaces the text for the item with the given ID.
The initial state should also be defined and passed to the reducer function as the default value for the state variable. Of course, this doesn’t define your entire Redux state structure, only the state.todos section.
These three files are usually separated in more complex apps, but if you want, you can also define them all in one file, just make sure you’re importing and exporting properly.
With that feature complete, let’s hook it up to Redux (and to our app). In /store/root-reducer.js, import the todosReducer (and any other feature reducer from the /features/ folder), then pass it to combineReducers(), forming one top-level root reducer that is passed to the store. This is where you’ll set up the root state, making sure to keep each feature on its own branch.
Using Redux In React
Of course, none of this is useful if it’s not connected to React. To do so, you’ll have to wrap your entire app in a Provider component. This makes sure that the necessary state and hooks are passed down to every component in your app.
In App.js or index.js, wherever you have your root render function, wrap your app in a
You are now free to use Redux in your components. The easiest method is with function components and hooks. For example, to dispatch an action, you will use the useDispatch() hook, which allows you to call actions directly, e.g. dispatch(todosActions.addTodo(text)).
The following container has an input connected to local React state, which is used to add a new todo to the state whenever a button is clicked:
Once you have everything set up and figured out, you might want to look into setting up Redux Devtools, setting up middleware like Redux Logger or connected-react-router, or installing a side effect model such as Redux Sagas.