DEV Community

Cover image for Mastering useReducer (1/2)
Pedro Figueiredo
Pedro Figueiredo

Posted on • Updated on

Mastering useReducer (1/2)

This blog post takes for granted that you have some knowledge regarding React and React's Hooks.

Managing state in React

As you probably know, React has 2 ways to manage state:

Both are widely used across any given React application, and although they ultimately serve the same purpose (managing state), they should be used in different situations.

When to use useReducer vs useState

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. - React's docs

As stated in the paragraph above, the useReducer hook should be opted in when the logic behind your state is a bit more complex or depends on the previous state.

✅ Good use cases for useReducer:

  • Changing 1 piece of state also changes others (co-related state values);
  • The state is complex and has a lot of moving parts;
  • When you want/need more predictable state transitions;

The useReducer hook

Now that we have some context on where to use this hook, it's time to take a closer look at it's API.

useReducer it's a built in function brought by React that has 2 different signatures:

  • useReducer(reducer, initialArg);
  • useReducer(reducer, initialArg, init);

useReducer arguments

reducer

The reducer as it's own name indicates, it's a function that takes some information and reduces it into something, and this is the place where the "magic" happens.

It takes two arguments, the current state and the action which is dispatched by the UI. By taking a given action type, a reducer will return the next piece of state, usually by deriving the previous state.

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
  }
}
Enter fullscreen mode Exit fullscreen mode

initialState

This argument is pretty self explanatory, it's just the state that the useReducer hook will start with.

init

init is a function that allows you do some logic around the initial state, as it will take the value you passed as initialState and return a "new" initialState based on that.

function init(initialCount) {
  return {count: initialCount};
}
Enter fullscreen mode Exit fullscreen mode

useReducer returned values

Very similar to useState, this hook returns an array with two values:

  • The first, to show the current state;
  • The second, a way to change the state, and create a re-render in the application.
 const [state, dispatch] = useReducer(counterReducer, initialState);
Enter fullscreen mode Exit fullscreen mode

state

This value doesn't need much explanation, it is simply the current state returned by the useReducer hook.

dispatch

This is a function where you can pass the the possible actions that you define for your reducer to handle. Taking the previous counterReducer by example, these could look like this:

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Managing the fetching logic with the useReducer hook

Now that we have a better understanding of what the useReducer hook can do for us, it's time to get our hands dirty and make usage of this React hook to handle any given fetching related state.

Fetching state

In order to use useReducer, you must first think what will be the state that you want to manage, these are usually all the things that you might have in a bunch of useState hooks, like data, errorMessage, fetchState, etc...

In this scenario, as we want to create a hook that will allow us to manage fetching logic. And as far as fetch logic goes, all the pieces you need are:

  • state: to know if the application is iddle, loading, if the fetch was a success or a failure
  • error: a error message in case something went wrong
  • data : the response data

And so, now that we have our state structure defined, we can setup our initialState.

// "iddle" state because we haven't fetch anything yet!
  const initialState = {
    status: "idle",
    data: null,
    error: null,
  };
Enter fullscreen mode Exit fullscreen mode

Fetching reducer

Actions

The second step, is to create the logic that will lead to different app states. That logic lives under the reducer function and for us to mount that logic, we should start by thinking on the "actions" that we need to perform.

For the fetching logic, we will need the following actions:

  • FETCH: action to be called when the request starts;
  • RESOLVE: action to be called if the response is successful;
  • REJECT: action to be called if the requests throws an error or the response is "invalid";

Bear in mind that you can call these actions whatever you want, as long as they reflect what's being done and it makes sense for you.

State transitions

Each of these actions (FETCH, RESOLVE and REJECT) will lead to a state transition, thus, producing a new output (a new state).

So now, it's just a matter of figuring out which will be the state that each of these actions will output.

FETCH
FETCH state transition


RESOLVE
RESOLVE state transition


REJECT
REJECT state transition


Implementing useReducer

With all the pseudo-code and decisions we have made above, we are now able to take advantage of useReducer to manage the fetching logic:

  const initialState = {
    status: "idle",
    data: null,
    error: null
  };

  function fetchReducer(currentState, action) {
    switch (action.type) {
      case "FETCH":
        return {
          ...currentState,
          status: "loading"
        };
      case "RESOLVE":
        return {
          status: "success",
          data: action.data,
          error: null
        };
      case "REJECT":
        return {
          data: null,
          status: "failure",
          error: action.error
        };
      default:
        return currentState;
    }
  }

  const [state, dispatch] = React.useReducer(fetchReducer, initialState);
}
Enter fullscreen mode Exit fullscreen mode

Fetching data

The implementation code is done, let's now check how would the code look look if we were fetching some data through our useReducer.

  function fetchIt() {
    // Start fetching!
    dispatch({ type: "FETCH" });
    fetch("https://www.reddit.com/r/padel.json")
      .then((response) =>
        response.json().then((result) => {
          // We got our data!
            dispatch({ type: "RESOLVE", data: result });
        })
      )
      .catch((error) => {
       // We got an error!
        dispatch({ type: "REJECT", data: error });
      });
  }

return (
    <>
      {state.status === "loading" ? <p>loading...</p> : undefined}
      {state.status === "success" ? <p>{JSON.stringify(state.data)}</p> : undefined}
      {state.status === "failure" ? <p>{JSON.stringify(state.error)}</p> : undefined}
      <button disabled={state.status === "loading"} onClick={fetchIt}>
        Fetch Data
      </button>
    </>
  );
Enter fullscreen mode Exit fullscreen mode

Creating useFetchReducer custom hook

Now, you will probably want to use this very same code to control your application's state in every place where you are performing an HTTP request.

Luckily for us, React brings a huge composition power packed in, making our life's quite simple when creating custom hooks through other existing React hooks (useReducer in this case).

Extracting useReducer hook

The 1st step, is to create a new file named use-fetch-reducer.js or whatever you wanna call it, as long and it starts with use (to be identified as an hook).

The 2nd step, is to take (copy) all the code that we implemented before, and paste it inside an exported function with the name useFetchReducer. It should look something like this:

import React from "react";

export function useFetchReducer() {
  const initialState = {
    status: "idle",
    data: null,
    error: null
  };

  function fetchReducer(currentState, action) {
    switch (action.type) {
      case "FETCH":
        return {
          ...currentState,
          status: "loading"
        };
      case "RESOLVE":
        return {
          status: "success",
          data: action.data,
          error: null
        };
      case "REJECT":
        return {
          data: null,
          status: "failure",
          error: action.error
        };
      default:
        return currentState;
    }
  }

  const [state, dispatch] = React.useReducer(fetchReducer, initialState);
}
Enter fullscreen mode Exit fullscreen mode

The 3rd step is to take out our useReducer result and return it instead, so that we can use state and dispatch in every other component:

//...
return React.useReducer(fetchReducer, initialState);
Enter fullscreen mode Exit fullscreen mode

To wrap things up, we should make this hook as "generic" as possible, so that it can satisfy the need of every component where it's being called from. In order to get there, the 4th step passes by providing a way for consumers to set the initialData themselves, because it might not always start as null:

function useFetchReducer(initialData = null) {
  const initialState = {
    status: "idle",
    data: initialData,
    error: null
  };

//...
Enter fullscreen mode Exit fullscreen mode

Using useFetchReducer

  1. Import the newly created hook into your component;
  2. Execute it as const [state, dispatch] = useFetchReducer();
  3. Use it's state and dispatch as you would for the useReducer hook.

Running Code
useFetchReducer

Conclusion

If your app state is becoming somewhat complex and the number of useState is mounting up, it might be time to do a small switch and take advantage of useReducer instead.

If you have decided to use useReducer, follow these steps:

  1. Think of the State you want to manage;
  2. Think of the Actions that that will trigger state transitions;
  3. Think for the State Transitions that will happen when calling the defined set of states.

With these thought out, it's time to write your own reducer and call the useReducer hook.

If the logic you just created can be reused across your application, create a custom hook and enjoy 😉


The 2nd part of this series will bring some type safety to the table, make sure to follow me on twitter if you don't want to miss it!

P.S. the useFetchReducer code was highly inspired into David K. Piano's code, present in this great blog post.
_

Top comments (1)

Collapse
 
estrng profile image
José Ivan R. de Oliveira

The power of useRreducer! 🐱‍👤