-- online Search!

Introduction to Redux

by kirupa   | filed under Learning React

The greatest love story of all time is not between Romeo and Juliet. It's not between any characters we've seen in books or movies. It's actually between React and a mysterious unknown straggler from a far-away land known as Redux:

Shakespeare writing a love story

By now, we know enough about React to understand how it works and why it does some of the things it does. We also know how to keep state inside a component with useState, how to organize more complicated updates with useReducer, and how to share data across a bunch of components with context. Redux is a different take on the same problem, and it is one that we haven't talked about at all. We need to fix that before we can figure out why React and Redux are so often mentioned in the same breath.

There is one more thing I want to say up front. Redux used to be the default answer for state in a React app. It isn't anymore, and that's fine. React itself has gotten much better at handling state, and there are a few smaller libraries that do a similar job with less ceremony. We'll look at when Redux makes sense (and when it doesn't) toward the end. For now, our goal is to understand how it works, because the ideas inside Redux show up in a lot of places, including the useReducer hook we've already seen.

Onwards!

What is Redux?

If there is one thing we've learned in all this time, it is this: maintaining application state and keeping it consistent with our UI is a major challenge. Solving this is partly why libraries like React really took off. If we cast a wider net and look beyond just the UI layer, you'll see that maintaining application state in general is complicated. Your typical app has many layers, and each layer has its own dependency on some piece of data that it relies on to do its thing.

Often, visualizing the relationship between your app's functionality and its application state would look pretty confusing:

A tangled mess of lines connecting an app to its data

To solve this more general problem of maintaining application state, you have Redux. The easiest way to understand how Redux works is by just walking through the various pieces that go into it. The first thing we are going to need is an app:

A blank app window

Our app doesn't have to be anything special. It can be built in React, Angular, Vue, vanilla JS, or whatever happens to be the hot new library/framework this week. Redux doesn't care how your app is built. All it cares about is that your app has a place to put its application state. In the Redux world, we store all our application state in a single location that we will just call the store:

A store sitting next to the app

The thing about the store is that it is very easy to read data from it. Getting information into it is a whole another story. The way you add new state to (or modify existing state in) our store is by using a combination of actions, which describe what happened, and a reducer that determines what the final state will be for a given action. When we throw both of these into the picture, this is what we see:

The app sends an action to a reducer, and the reducer updates the store

There are a few more moving pieces that this diagram doesn't highlight, but it is a good approximation of what happens when our app needs to update the state stored in our store. Now, looking at this diagram, you are probably wondering why there is all this roundaboutness and indirection. Why can't our app just update the store directly:

An app modifying the store directly

The reason is predictability. When every part of your app can reach in and change the data whenever it wants, finding out who changed something (and why) turns into a murder mystery. With Redux, every change goes through the same narrow doorway. This roundabout way is Redux's answer for helping make sure storing application state is manageable for both simple apps as well as complex ones. This is what that predictability boils down to:

  1. Your entire application's state is stored in a single location. You don't have to search across a variety of data stores to find the part of your state you want to update. Keeping everything stored in a single location also ensures you don't have to worry about keeping all of this data in-sync!
  2. Your state is read-only and can only be modified through actions. As we saw in the diagram earlier, in a Redux world, random parts of your app can't access the store and modify the state stored inside it. The only way our app can change what is in the store is by sending an action.
  3. Changes are made by reducers, which are plain functions. A reducer takes the current state and an action, and it works out what the next state should be. It doesn't reach out to the network, it doesn't read the clock, and it doesn't quietly change anything outside itself.

These three principles may seem a bit abstract, but as we start to write some Redux code, you'll see us put them into practice.

Redux vs. Redux Toolkit

Before we write any code, we need to sort out some naming. You'll see two names floating around: Redux and Redux Toolkit. The redux package is the small core library that implements the store, actions, and reducers idea we just looked at. It is intentionally bare-bones. If you use it by itself, you end up writing a lot of repetitive code (action type strings, switch statements, and careful copying of objects and arrays) just to change one value.

Redux Toolkit is the layer on top that removes most of that repetition. It is what the Redux team recommends you use, and it is the package we are going to install. It sets up the store for you with some helpful defaults, and it lets you define actions and reducers together in one place. You'll see this in action in a moment.

If you come across older Redux tutorials (or code in an older project), you may see a function called createStore being used directly. That still works, but it is marked as deprecated, and Redux Toolkit's configureStore is what you use instead.

Building a Simple App Using Redux

What we are going to do is take all of the diagrams and text we saw in the previous section and turn them into code. The app we are going to build to highlight how Redux works is going to be a really simple console-driven app without any UI. It is an app that will store a list of our favorite colors. From our app, you can add colors, and you can remove colors. That's pretty much it.

While this may seem like a step backwards from the UI-rich apps we have been building so far, this app will tie together all of this theoretical Redux knowledge into some tangible lines of code. The goal is to simply make sense of Redux. Redux has nothing to do with React, so we don't need React yet. We'll combine the two in the next tutorial.

Setting Things Up

All we need is Node.js and a terminal. From your terminal or command prompt, create a new folder, initialize it, and install Redux Toolkit:

mkdir favoritecolors
cd favoritecolors
npm init -y
npm install @reduxjs/toolkit

Once that is done, create a new file in this folder called favoriteColors.mjs. The .mjs extension tells Node that this file uses modern import statements, so we don't have to fiddle with any other settings.

Our Slice

The first things we are going to define are our actions and our reducer. Remember, the action is the only mechanism we have to communicate with our store. Because we want to add and remove colors, our actions need to represent those two wishes in a way the store will understand.

In Redux Toolkit, you define actions and the reducer that handles them together using a function called createSlice. A slice is simply the portion of your app's state that one piece of logic is responsible for. Ours is responsible for the list of colors. Add the following lines to favoriteColors.mjs:

import { configureStore, createSlice } from "@reduxjs/toolkit";

const colorsSlice = createSlice({
  name: "colors",
  initialState: [],
  reducers: {
    addColor: (state, action) => {
      state.push(action.payload);
    },
    removeColor: (state, action) => {
      return state.filter((color) => color !== action.payload);
    },
  },
});

export const { addColor, removeColor } = colorsSlice.actions;

Take a moment to walk through what this code is doing. We call createSlice and give it an object with three things:

  • name is a label for this slice. It gets used when Redux Toolkit builds the action type strings for us.
  • initialState is what our state looks like before anything happens. For us, it is an empty array. You can use any data structure you want, but an array is the right one for what we are trying to do.
  • reducers is an object where each function is a case our slice knows how to handle. Our slice knows how to addColor and removeColor.

Each of those reducer functions receives the current state and the action that was fired. The color we care about is stored in action.payload. For addColor, we add that color to the array. For removeColor, we return a new array that has the color in question left out.

The last line of that code is interesting. Remember how we said that actions are what we use to tell the store what happened? A slice creates the action creator functions for us automatically. We pull them out of colorsSlice.actions so we can use them. An action creator is a function that, um...creates an action. To see what that looks like, you can log the result of calling one:

console.log(addColor("blue"));

What you would see in the console is the action itself, which is just a plain object:

{ type: 'colors/addColor', payload: 'blue' }

Every action is an object with a type property that says what happened. Beyond that, you can put whatever else you want on it. By convention, Redux Toolkit uses a property called payload for the extra data. The type is built by combining the slice name (colors) with the name of the reducer function (addColor).

The Immutability Part

Now take another look at our addColor reducer. It calls state.push, which modifies the array. That looks like it breaks the rule we talked about earlier. Reducers aren't supposed to change the existing state. They are supposed to calculate a brand new one. So what is going on?

As it turns out, Redux Toolkit uses a library called Immer behind the scenes. The state you receive inside a createSlice reducer isn't your real state. It is a stand-in. You can write code that looks like it modifies the stand-in, and Immer watches what you do and builds a new, separate copy of the state with those changes applied. Your original state stays untouched.

This idea of never changing the existing state is called immutability, and it matters for two reasons. The first is that it makes changes easy to track. If the old state and the new state are two different objects, Redux (and React) can tell that something changed by simply comparing them. The second is that it makes reasoning about your app easier. Nothing can sneak in and change your data behind your back.

There is one rule to keep in mind when you write these reducers. Inside a reducer, you can either change the stand-in (like we did with push) or return a new value (like we did with filter), but you shouldn't do both in the same function. Either approach gets you to the same place, so use whichever reads more clearly to you.

The other thing to keep in mind is what a reducer is allowed to do. The Redux documentation boils it down well:

Things you should never do inside a reducer:

  • Perform side effects like API calls and routing transitions
  • Call non-pure functions, like Date.now() or Math.random()
  • Change anything that lives outside the reducer

Given the same state and the same action, a reducer should calculate the next state and return it. No surprises. No side effects. No API calls. Just a calculation.

Creating the Store

All that remains now is to tie our slice with a store! Below your slice, add the following:

const store = configureStore({
  reducer: {
    colors: colorsSlice.reducer,
  },
});

What we are doing here is creating a new store by using the configureStore function. The reducer property is an object where each key is a piece of our state and each value is the reducer responsible for it. We gave our reducer the key colors. This means that our full state will look like { colors: [...] }. If we had more slices (a user slice, a settings slice, and so on), they would each get their own key here.

Behind the scenes, configureStore does a bit more than just create a store. It turns on the Redux DevTools connection for you, and while you are developing, it adds checks that yell at you if you accidentally mutate your state or put something strange in it.

Once you've done this, we've come full circle with using Redux to store our application state. We have our store, we have our reducer, and we have actions that tell our reducer what to do. Here is the whole sequence of what happens when our app wants to change something:

The app dispatches an action, the reducer uses it along with the previous state to produce the next state, and the store notifies subscribers

Dispatching Actions

To see everything fully working, we are going to add (and remove) some colors in our store. To do this, we use the dispatch method on our store object. It takes an action as its argument. Go ahead and add the following lines:

store.dispatch(addColor("blue"));
store.dispatch(addColor("yellow"));
store.dispatch(addColor("green"));
store.dispatch(addColor("red"));
store.dispatch(addColor("gray"));
store.dispatch(addColor("orange"));
store.dispatch(removeColor("gray"));

Each dispatch call sends an action to our reducer. Our reducer takes the action and performs the appropriate work to define our new state. To see our store's current state, you can just add the following after all of our dispatch calls:

console.log(store.getState());

The getState method, as its name implies, returns our state's value. Save your file, and then run it from your terminal:

node favoriteColors.mjs

You should see the colors we added (minus gray, which we removed) displayed in the console:

{ colors: [ 'blue', 'yellow', 'green', 'red', 'orange' ] }

Notice that the array is sitting inside an object under the colors key. That's thanks to the key we gave our reducer when we created the store.

Subscribing to Changes

We are almost done here. There is just one more really important thing we need to cover. In real world scenarios, you will want to be notified each time our application's state has been modified. This push model makes our lives much easier if we want to update UI or perform other tasks as a result of some change to our store. To accomplish this, you have the subscribe method that allows you to specify a function (aka a listener) that gets called each time the contents of our store get modified.

To see subscribe in action, replace your console.log(store.getState()) line with the following, and place it just after you create your store object (above all of your dispatch calls):

store.subscribe(() => {
  console.log(store.getState());
});

After you've done this, run your app again. This time, each time we call dispatch to fire another action, our listener gets called, and you will see the state as it changes one step at a time:

{ colors: [ 'blue' ] }
{ colors: [ 'blue', 'yellow' ] }
{ colors: [ 'blue', 'yellow', 'green' ] }
{ colors: [ 'blue', 'yellow', 'green', 'red' ] }
{ colors: [ 'blue', 'yellow', 'green', 'red', 'gray' ] }
{ colors: [ 'blue', 'yellow', 'green', 'red', 'gray', 'orange' ] }
{ colors: [ 'blue', 'yellow', 'green', 'red', 'orange' ] }

This is exactly what a UI library needs. When something changes, it gets told about it. In the next tutorial, you'll see that React's Redux bindings take care of subscribing for us, so we won't have to call subscribe ourselves.

Seeing Immutability in Action

Earlier, we talked about how the state is never modified. We can see that for ourselves. Add the following lines to the end of your file:

const before = store.getState();
store.dispatch(addColor("purple"));
const after = store.getState();

console.log(before === after);
console.log(before.colors.includes("purple"));

When you run this, you will see one more state update (with purple added), followed by false and then false. The first false tells us that before and after are two different objects. The second one tells us that the old state never got the new color. Our reducer used push, but the state we are holding on to was left alone.

Redux Toolkit also freezes the state it gives you, so if you try to modify it directly from outside a reducer (for example, by calling store.getState().colors.push("hotpink")), JavaScript will throw a TypeError at you. This is a good thing. The only way to change the state is by dispatching an action.

Do You Need Redux?

Now that you know how Redux works, you have probably been wondering whether it is something you should add to your own projects. The honest answer is: maybe. Redux solves a real problem, but it also adds some extra concepts and files that you need to maintain. Whether it is worth it depends on what you are building.

Redux is great when:

  • Lots of different parts of your app need to read and change the same state.
  • Your state updates have a lot of rules, and you want those rules to live in one place where they can be tested.
  • You want to see exactly what happened in your app and in what order. The Redux DevTools let you replay a list of actions and step backwards and forwards through your state.
  • You are working on a larger app with a larger team, and you want a predictable pattern everyone follows.

Redux is probably more than you need when:

  • Your state is mostly local to a component or two. For that, useState is simpler.
  • You just need to pass a few values down a deep tree. Context was built for that.
  • Your state logic is complicated, but it is still contained inside one part of your app. The useReducer hook gives you the same reducer-and-action pattern we used here without installing anything.
  • Most of your "state" is really data that lives on a server. Libraries made for fetching and caching data (like TanStack Query, or RTK Query, which is included with Redux Toolkit) handle that much better than hand-written reducers.

There are also other libraries that handle shared client-side state with a lot less setup. Zustand is a popular one. You create a small store, and you read from it directly inside your components. It doesn't have the action and reducer layer that Redux does, so there is less to write. The tradeoff is that you also get less structure, which may or may not be what you want.

Dan Abramov, one of the creators of Redux, wrote a great article explaining some of the situations when you probably shouldn't use Redux. It was written a while ago, but the core idea still holds up. Start with the simplest thing that works, and reach for Redux when you feel the pain it solves.

Conclusion

We've taken a whirlwind tour of Redux and the major pieces of functionality it brings to the table. We looked at the concepts that make Redux useful: a single store, actions that describe what happened, and reducers that work out the next state without modifying the old one. We also looked at the code to make everything real by using Redux Toolkit's createSlice and configureStore. Along the way, we saw what immutability means and why the store works so hard to protect it.

The only thing we didn't get to do was create a more realistic example with a UI. Redux works with any UI library, and each UI library has its own way of connecting to it. Our UI library of choice is, of course, React! That is what we will look at next.

Next tutorial: Using Redux with React

Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence slop, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.

Your support keeps this site going! 😇

Kirupa's signature!

Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over a bazillion subscribers!

SUBSCRIBE NOW

Creating engaging and entertaining content for designers and developers since 1998.

Follow:

Popular

Loose Ends

:: Copyright KIRUPA 2026 //--