DEV Community

loading...
Cover image for Try using reducers for state management.

Try using reducers for state management.

tusharkashyap63 profile image Tushar Kashyap ・4 min read

Ever heard about reducers before?

Well of course bud, what're you talking about? I'm head over heels in love with reducers. I use reducers everywhere, even in the simplest counter app.

Well, great! This article isn't for you 😊.

If you've heard about reducers and are trying to procrastinate from the moment you heard about reducers because you don't feel the need to learn about this overly complex, intricate, tortuous and difficult sounding thing and actually stick useState everywhere you want to manage state, you're at luck 🍀.

Reducers are a pretty fun way of managing state(as I feel) with a variety of benefits as we shall see throughout the article.

So what is a Reducer?

Well, it's just a function. Nothing fancy. It takes in the parameters and spits out a return value. As every normal function does.

Let us look at the params and the return value, shall we?

So every reducer function takes in the initial state of the application and the action which is like saying "Here's the thing that happened." and throws out the new state of the application after the action has occurred.

Now after making this reducer function you'll just have to dispatch actions to bring changes to the state. That's pretty much what is the reducer pattern.

Time to build!

OKAY! Let's make a div and change its CSS properties using a reducer. What I like to do is making the reducer in isolation, thinking of what actions I would like to perform.

const INCREASE_WIDTH = "INCREASE_WIDTH";
const TOGGLE_FILL = "TOGGLE_FILL";

const cssReducer = (state, action) => {
  if (action.type === INCREASE_WIDTH) {
    return { ...state, width: state.width + 5 };
  }

  if (action.type === TOGGLE_FILL) {
    return {
      ...state,
      backgroundColor: state.backgroundColor === "white" ? "plum" : "white"
    };
  }

  return state;
};
Enter fullscreen mode Exit fullscreen mode

So here I would like to increase the width of my box and also toggle the background color, as you can see I have defined two actions types on top INCREASE_WIDTH and TOGGLE_FILL which are just strings and are there to prevent typos as an error message will pop up as soon as you mess up the name of the constant whereas none would've popped if you were using strings everywhere and misspelt one. Also, they help in autocompleting stuff so just do it 👍🏻.

The reducer function is taking in the initial state and as per the type of action it changes the state accordingly and returns it which will update the view.

You see the benefit? As it is just a function it is utterly easy to test, just provide it with the initial state object and an action object and it will be throwing out a predictable final state object. Smooth 🧈.

P.S. It's okay if you're a little confused as you've just seen a reducer function and this will make sense when we implement it fully in our application, that's up next.

The useReducer hook!

useReducer is one of the prebuilt React hooks, which let us implement reducer patterns quite easily without reaching out for external libraries like Redux.

Obviously it is missing some features that Redux has(applying middleware for example) out of the box, so it depends if you need those in your application. Most of the time you don't, but this is a choice you've got to make.

Here's how to use it

const [styles, dispatch] = useReducer(cssReducer, initialStyles);
Enter fullscreen mode Exit fullscreen mode

useReducer takes in the reducer and the initial state as its arguments and returns an array of two things, first one is the state of the application at any given time and second is the dispatch function used to dispatch actions.

Let us write all the code together so you can see dispatch functions in action.

import React, { useReducer } from "react";

const INCREASE_WIDTH = "INCREASE_WIDTH";
const TOGGLE_FILL = "TOGGLE_FILL";

const initialStyles = {
  border: "3px solid plum",
  height: 100,
  width: 100,
  backgroundColor: "white"
};

const cssReducer = (state, action) => {
  if (action.type === INCREASE_WIDTH) {
    return { ...state, width: state.width + action.payload.step };
  }

  if (action.type === TOGGLE_FILL) {
    return {
      ...state,
      backgroundColor: state.backgroundColor === "white" ? "plum" : "white"
    };
  }

  return state;
};

export default function App() {
  const [styles, dispatch] = useReducer(cssReducer, initialStyles);

  return (
    <div className="App">
      <div style={styles}></div>
      <button
        onClick={() => {
          dispatch({
            type: INCREASE_WIDTH,
            payload: {
              step: 10,
            }
          });
        }}
      >
        Increase Width
      </button>
      <button
        onClick={() => {
          dispatch({
            type: TOGGLE_FILL
          });
        }}
      >
        Toggle Fill
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here we are dispatching actions on click of the buttons. The action object should at least contain a type but we can also pass in more info about the action commonly in a key named payload, as we pass in the step here and we have changed our reducer function slightly to use that key.

Here is a live demo to play around with.

Another advantage we can see instantly is that it pulls out all the logic from the component itself which is now just rendering the HTML. You can put the reducer logic in another file(maybe even a file for all the actions) to make the code even more maintainable.

Reducers are really useful where there are a lot of moving parts, like in case of a form with a lot of fields, instead of using a useState for every field try using a reducer instead.

You can also use a reducer to fetch from an external source and handle all the different stages of the request.

Here's a take on it.

import React, { useEffect, useReducer } from "react";

const REQUEST_LOADING = "REQUEST_LOADING";
const FETCH_SUCCESS = "FETCH_SUCCESS";
const FETCH_FAILURE = "FETCH_FAILURE";

const initialState = {
  loading: false,
  data: null,
  error: null
};

const fetchReducer = (state, action) => {
  if (action.type === REQUEST_LOADING) {
    return {
      date: null,
      loading: true,
      error: null
    };
  }

  if (action.type === FETCH_SUCCESS) {
    return {
      data: action.payload.response,
      loading: false,
      error: null
    };
  }

  if (action.type === FETCH_FAILURE) {
    return {
      data: null,
      error: action.payload.error,
      loading: false
    };
  }

  return state;
};

export default function App() {
  const [{ loading, data, error }, dispatch] = useReducer(
    fetchReducer,
    initialState
  );

  useEffect(() => {
    dispatch({ type: REQUEST_LOADING });

    fetch("some url")
      .then((res) => res.json())
      .then((response) => {
        console.log(response);
        dispatch({ type: FETCH_SUCCESS, payload: { response } });
      })
      .catch((err) => {
        dispatch({ type: FETCH_FAILURE, payload: { error: err } });
      });
  }, []);

  if (error) return <p>{error.message}</p>;

  return (
    <div className="App">
      {loading ? <p>Loading...</p> : <p>{data.setup}</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Using reducers have certain benefits if used in the right places but don't stick them everywhere like the guy in the beginning. useState is perfectly fine for simple state management.

Discussion (4)

pic
Editor guide
Collapse
angelomiranda profile image
angelo.miranda

nice, i made a video of array reduce to help beginners to get the concept. It is one of the array method i had a hard time understanding at first but now is using as needed - youtu.be/SGjIG1ZiH2w

Collapse
dakshguru_ profile image
Collapse
farismecinovic profile image
Faris Mecinovic

Nice explanation

Collapse
tusharkashyap63 profile image
Tushar Kashyap Author

Glad you liked it 😊