Skip to main content
Donate to support Ukraine's independence.

An introduction to Redux with React

Many React applications (if not most of them) need to manage some sort of state in order to be fully functional. When state changes, the DOM must also change. Moreover the information displayed might be different and specific to a certain state. Generally we can manage state in React using some hooks like useState and useContext. More specifically the React Context API is very useful for managing non-local component-wide (or app-wide) state, but it has some cons. This is where Redux comes in.

What is Redux? When should we use it?

Redux is a state management system for cross-component or app-wide state. Sounds like the Context API, right? Yes, but Redux can be used where the Context API might be inappropriate. Indeed the Context API might lead to deeply nested components if we have multiple contexts in order to manage different states, with code that might look like this:

return (
    <AuthContextProvider>
        <ThemeContextProvider>
            <SomeOtherContext>
                <YetAnotherContext>
                    <RegistrationForm />
                </YetAnotherContext>
            </SomeOtherContext>
        </ThemeContextProvider>
    </AuthContextProvider>
)

Well, “just use a single Context to manage all the things!” you say. Yeah, it would work, but then the setup gets complex and the code becomes very difficult to maintain. Finally I don’t know if things have changed with the Context API, but a few years back it was known that the Context API wasn’t good for high frequency changes.

A React dev talking about the Context API

To summarize, if you need to manage state across different components (or app-wide) and this state changes frequently or is made up of different “state slices”, then you should consider using Redux.

Redux basic concepts and how it works

Redux works by making use of a so called Central Data Store, where all the different pieces of state (such as theming, authentication, cart, etc.) are stored. Components can subscribe to this store in order to get notified when data changes and be able to use it.

Important
Components never directly manipulate the central data store.

For changing and manipulating the data inside the store we can use the reducers, which are special functions responsible for mutating the data store. Components can then dispatch actions (trigger state changes), which are simple Javascript objects describing the kind of operation the reducers should perform. Redux then forwards the action to the reducer, which reads the description and performs the operation.

A brief word regarding the old way of using Redux

In the old days, the store was created by using const store = createStore(someReducerFunction) and defining the reducer function with a switch-like structure accessing the action string, which was custom made. This approach can still be followed today, but it is deprecated and the Redux team suggests using Redux Toolkit. Why is it deprecated? I suppose because it is error-prone; indeed in order to dispatch an action from the component, we would need to remember the exact strings specified in the reducer function. Moreover Redux was designed to handle actions in an immutable way, meaning that the reducer function should not mutate the state, but return a new brand new one, specifying each time the different slices. Basically, the reducers are pure functions, with no side-effects happening inside them. Instead Redux Toolkit allows us to write code which seems to mutate the state, but behind the scenes Redux does the heavy lifting by changing our state mutating code to code that respects immutability.

Using Redux Toolkit and react-redux

Here we will focus on an example which uses the aforementioned Redux Toolkit and the package react-redux. For this reason, if you want to follow along, you should install these packages by typing npm install @reduxjs/toolkit react-redux into the terminal and inside the project folder. All the code used in this post can be found here.

The example will focus on an application which buils a message paragraph by adding words taken from a random word API. We will use buttons to modify the state and show how dispatching actions works.

Creating the reducer function and the actions

First of all, we need to define our state slices. In our case we have a single slice, but we are still gonna use different files for the slices and the store to show how we can separate different state slices in different files.

In our case we have a message slice, which is initially empty, and the reducers are functions that will allow us to add a word, remove a word or reset the state. So in our message.js (inside the store folder) we have the following code.

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

const initialMessageState = { message: "" };

const messageSlice = createSlice({
  name: "message",
  initialState: initialMessageState,
  reducers: {
    addWord(state, action) {
      if (state.message === "") {
        state.message = action.payload;
      } else state.message = `${state.message} ${action.payload}`;
    },
    removeWord(state) {
      state.message = state.message.split(" ").slice(0, -1).join(" ");
    },
    reset(state) {
      state.message = initialMessageState.message;
    },
  },
});

export const messageActions = messageSlice.actions;

export default messageSlice.reducer;

To build the store we have instead the following code inside the index.js (store folder):

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

import messageReducer from "./message";

const store = configureStore({
  reducer: messageReducer,
});

export default store;

As you can see we could easily define different state slices in different files and then define a single store like this:

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

import messageReducer from "./message";
import otherReducer from "./other";

const store = configureStore({
  reducer: {
    message: messageReducer,
    other: otherReducer,
  },
});

export default store;

The Message Component

We now want a React components which is able to access the message in the store and dispatch actions to modify it. More specifically we have three buttons: one for adding a word to the existing message, one to remove the latest word and one to reset the message.

The useSelector hook allows us to specify a function returning the specific state slice we want to have access to, while the useDispatch hook gives us access to the functionality needed to dispatch actions.

This is the Message.js component:

import { useSelector, useDispatch } from "react-redux";

import { messageActions } from "./store/message";

const Message = () => {
  const dispatch = useDispatch();
  const message = useSelector(state => state.message);

  const addWordHandler = async () => {
    const response = await fetch("https://api.api-ninjas.com/v1/randomword");
    const data = await response.json();
    dispatch(messageActions.addWord(data.word));
  };
  const removeLastWordHandler = () => {
    dispatch(messageActions.removeWord());
  };
  const resetMessageHandler = () => {
    dispatch(messageActions.reset());
  };

  return (
    <main>
      <h1>React Redux Message Builder</h1>
      <p>{message}</p>
      <div>
        <button onClick={addWordHandler}>Add new word</button>
        <button onClick={removeLastWordHandler}>Remove last word</button>
        <button onClick={resetMessageHandler}>Reset</button>
      </div>
    </main>
  );
};

export default Message;

As you can see, while dispatching actions we can refer directly to the reducers we have defined before, allowing us to use code completion and avoiding bugs caused by mispelling action type strings.

Providing access to the state to the components

Finally we need to define a way for the components to have access to the state; we can do that by importing the Provider from react-redux and wrapping the code that needs access to the store.

This is the App.js file:

import { Provider } from "react-redux";

import store from "./store/index";
import Message from "./Message";

function App() {
  return (
    <Provider store={store}>
      <div>
        <Message />
      </div>
    </Provider>
  );
}

export default App;

The final result

You can see the result by cloning the repo, and typing:

git clone https://github.com/DarthVi/react-redux-intro
cd react-redux-intro
npm install
npm start

In any case here’s a video:

Problems with screencapture
I had problems with the native Pop_OS (Gnome desktop environment) screen capture app, so some frames are missing. To fully appreciate how the example works, I advise you to clone the repository and run it locally.

Send a like: