How to Implement Redux Reducers for React

How to Implement Redux Reducers for React

by Godwin Chinda

If you plan to use Redux as a state management tool, you'll have to deal with Flux concepts like reducers and actions. In this article, we will discuss what they are and how to better implement them for your React apps.

What is State?

The state is a type of object that specifies characteristics of your program that can change over time. You should make your state the bare minimum of data required to explain the application's current state. If Redux manages an application's state, state updates are handled by a reducer that processes actions, as we'll see below. For example, our chat application could be in the following state:

  {
    userId: "user-2022",
    username: "Great",
    messages: [
      {
        user: "Great",
        message: "Hi",
      },
      {
        user: "Hannah",
        message: "Hello there!",
      },
    ],
  };

What are Actions?

Actions are a simple JavaScript object that holds data. Actions have a type field that specifies the type of action to take, and all other fields contain data or information. Actions are the sole way for the Redux store to be updated with new information; they provide the payload for changes to an application store. Actions tell Redux what kind of action to perform, like the following:

const action = {
  type: "ADD_TO_CART",
  payload: {
    product: "doughnut",
    quantity: 6,
  },
};

The code above is a typical payload value that contains the data that a user sends and is used to change the application's state. As you can see from the example above, the action object contains the type of action and a payload object required for this action to be completed. Reducers update the store based on the action.type value; here, ADD_TO_CART.

What is a Reducer?

A reducer is a pure function in Redux that accepts an action and the application's previous state and returns the new state. The action specifies what occurred, and the reducer's role is to return the updated state as a result of that action. Pure functions have no side effects and will produce identical outcomes if the same arguments are handed in.

An example of a pure function is as follows:

const add = (a, b) => a + b;

add(3, 6);

The code above returns a value based on the inputs; for example, if you pass 3 and 6, you'll always get 9. As long as the inputs are the same, nothing else influences the result; this is an example of a pure function.

A reducer function that takes in a state and action is shown below:

const initialState = {};
const cartReducer = (state = initialState, action) => {
  // Do something here
};

Each time an action is dispatched, the state is updated in terms of its current value and the incoming values in the action.

Updating State Using Reducers

Let's have a look at the number counter below to see how reducers work:

const increaseAction = {
  type: "INCREASE",
};

const decreaseAction = {
  type: "DECREASE",
};

const countReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREASE":
      return state + 1;

    case "DECREASE":
      return state - 1;

    default:
      return state;
  }
};

Here, increaseAction and decreaseAction are actions that determine how the state is updated. Next, we have countReducer, a reducer function that takes an operation and a zero-valued initial state. We return a new state that is increased by one if the value of action.type is INCREASE, and a new state that is decremented by 1 if the value is DECREASE. We return state in circumstances where none of those conditions apply. The initial state is zero.

Updating State Using Reducers: The Spread Operator

State can't be directly altered. We can use the JavaScript spread operator to ensure that we don't modify the value of the state directly but instead return a new object with the state supplied to it plus the user's payload.

const contactAction = {
  type: "GET_CONTACT",
  payload: ["08123456789", "0901234567"],
};

const initialState = {
  contacts: [],
  contact: {},
};

export default function (state = initialState, action) {
  switch (action.type) {
    case GET_CONTACTS:
      return {
        ...state,
        contacts: action.payload,
      };
    default:
      return state;
  }
}

We use a spread operator in the code above to avoid changing the state value directly. This allows us to return a new object that is filled with the state that is supplied to it and the payload that the user sends. We can ensure that the state remains the same when adding all new items and changing the contacts field in the state (if previously present) by using a spread operator.

Open Source Session Replay

OpenReplay is an open-source alternative to FullStory and LogRocket. It gives you full observability by replaying everything your users do on your app and showing how your stack behaves for every issue. OpenReplay is self-hosted for full control over your data.

replayer.png

Happy debugging for modern frontend teams - start monitoring your web app for free.

Implement Reducers in our Github-Finder app

We'll be creating a Github-Finder app with Redux Reducers for a better understanding. We'll be working on our React app, and we'll be implementing a Reducers function. We'll be skipping the building of our app to save time, but you can find it at my GitHub repo. We've created our app, and its purpose is to get data from every GitHub user and their repositories. For us to get the USERS result when we search their names; that's where our Reducers come in.

github-finder app

So let's jump to our application as we want to create a specific file for our GitHub context reducer. In the context github folder, I'm going to create a new file called GithubReducer.js, where all of our GitHub-state-related reducers will go. We'll create a function called githubReducer.

//githubReducer.js//
const githubReducer = (state, action) => {
  switch (action.type) {
    case "GET_USERS":
      return {
        ...state,
        users: action.payload,
        loading: false,
      };

    default:
      return state;
  }
};

export default githubReducer;

We go to our githubContext.js; we'll be using our reducers here.

import { createContext, useReducer } from "react";
import githubReducer from "./GithubReducer";

const GithubContext = createContext();

export const GithubProvider = ({ children }) => {
  const initialState = {
    users: [],
    loading: true,
  };

  const [state, dispatch] = useReducer(githubReducer, initialState);

  return (
    <GithubContext.Provider
      value={{
        ...state,
        dispatch,
      }}
    >
      {children}
    </GithubContext.Provider>
  );
};

export default GithubContext;

We get a result if we search for a user in our github-finder app.

The final app

In the full code for the app, we also use the Reducer function for GET_USER, GET_USER_AND_REPOS, SET_LOADING, and CLEAR USERS. For the demo of this app click here.

Conclusion

Reducers are a crucial component of Redux state management because they allow us to construct pure functions to update specified portions of our Redux apps without causing side effects. We've covered the fundamentals of Redux reducers, their applications, and the core concept of reducers, state, and actions.

You can learn more about Redux reducers in the documentation. For the demo app click here

Resources

newsletter