State Management in Pure React: The One with Hooks

In the previous post, we discuss about managing state in React's class component. You might find it useful especially if you’re working with a legacy React codebase. In this second part of the series, we are going to explore managing state using React hooks.

Let's start with refactor the simple counter application with hooks:

src/Counter.js
const Counter = ({ max }) => {
  const [count, setCount] = React.useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(0);

  return (
    <main>
      <p>{count}</p>
      <section className="controls">
        <button onClick={increment}>Increment</button>
        <button onClick={decrement}>Decrement</button>
        <button onClick={reset}>Reset</button>
      </section>
    </main>
  );

If you are not familiar with the syntax of the two values from the returned array, consider reading about Javascript array destructuring. In the case of React, the React useState hook is a function which returns an array.

Array destructuring is just a shorthand version of accessing each item one by one. The React team chose array destructuring because of its concise syntax and ability to name destructured variables.

function getAlphabet() {
  return ['a', 'b'];
}

// no array destructuring
const itemOne = getAlphabet()[0];
const itemTwo = getAlphabet()[1];

// array destructuring
const [firstItem, secondItem] = getAlphabet();

Okay, So, what don't we have to do here?

  • We don't have to bind anything.
  • We dont need a reference to this.
  • We don't need a constructor at all.

It also turns out that useState setters can take functions too.

const increment = () => {
  setCount(c => c + 1);
};

Unlike using values, using functions also works the same way as it does with this.setState .

const increment = () => {
  setCount(c => c + 1);
  setCount(c => c + 1);
  setCount(c => c + 1);
};

Custom Hooks

useEffect allows us to implement some kind of side effect in our component outside of the changes to state and props triggering a new render.

This is useful for a ton of reasons:

  • Storing stuff in localStorage.
  • Making AJAX requests.

React’s useEffect Hook takes two arguments: The first argument is a function where the side-effect occurs. The second argument is a dependency array of variables.

Let's make a simple, useful implementation using this feature. Here is a function of getting state from localStorage:

const getStateFromLocalStorage = () => {
  const storage = localStorage.getItem('counterState');
  if (storage) return JSON.parse(storage);
  return { count: 0 };
};

We'll read the count property from localStorage.

const [count, setCount] = React.useState(getStateFromLocalStorage().count);

Now, we'll register a side effect.

React.useEffect(() => {
  localStorage.setItem('counterState', JSON.stringify({ count }));
}, [count]);

The coolest part about this is that it works for increment, decrement, and reset all at once.

Then, let's pulling it out into a custom hook:

const getStateFromLocalStorage = (defaultValue, key) => {
  const storage = localStorage.getItem(key);
  if (storage) return JSON.parse(storage).value;
  return defaultValue;
};

const useLocalStorage = (defaultValue, key) => {
  const initialValue = getStateFromLocalStorage(defaultValue, key);
  const [value, setValue] = React.useState(initialValue);

  React.useEffect(() => {
    localStorage.setItem(key, JSON.stringify({ value }));
  }, [value]);

  return [value, setValue];
};

Now, we just never think about it again.

const [count, setCount] = useLocalStorage(0, 'count');

Knowing more about custom hooks gives you lots of new options. A custom hook can encapsulate non-trivial implementation details that should be kept away from a component; can be used in more than one React component, and can even be open-sourced as an external library.

Reducers

A naive way to communicate between component is by passing the data into the child component all the way to the lowest component:

// Application.js

// this function is passed into child component all the way to the lowest component.
const toggleApproval = id => {
    setApprovals(
      absences.map(absence => {
        if (absence.id !== id) return absence;
        return { ...absence, approved: !absence.approved };
      })
    );
  };

  return (
    <div className="Application">
      <NewRequest onSubmit={addRequest} />
      <Absences absences={absences} onForgive={toggleApproval} />
    </div>
  );

It works, but there are two issues that we'd like to solve for.

  • Prop drillingAbsences needs to receive toggleApproval even though it will never use it. It's just passing it down to Absence.
  • Needless re-renders: Everything re-renders even when we just check a single checkbox. We could try to get clever with some of React's performance helpers—or we can just manage our state better.

Setting up the Debugging tools

  • Turn on the "Highlight updates when components render." feature in the React developer tools.
  • Notice how a checking a checkbox re-renderers everything.
  • Notice how this is not the case in NewRequest.

We could try to get clever here with useCallback and React.memo, but since we're always replacing the array of requests, this is never really going to work out.

Introducing Reducers

Reducer is a function that takes two arguments, a current state, and an action, and demonstrates how to write a reducer function. With a reducer the state management of the state from the components rendering the the state.

Reducers

In useState the state management happen in the component while also showing the markup. divorcing the state management from the component.The big win of using reducers is that, reduce function can be unit tested. Usually unit test is not hard, but sometimes we writes code that is not easy to unit test.

Using the reducer

What if we took a different approach to managing state?

Let's make a new file called reducer.js.

reducer.js
const reducer = (state = [], action) => {
  return state;
};

And then we swap out that useState with a useReducer.

const [absences, dispatch] = useReducer(reducer, initialState);

We're going to create an action type and an action creator.

Action Type
const ADD_REQUEST = 'ADD_REQUEST';
const APPROVE_REQUEST = 'APPROVE_REQUEST';
const addRequest = ({ person, reason }) => {
  dispatch({
    type: ADD_REQUEST,
    payload: {
      person,
      reason
    }
  });
};

Action keys manage the state, a dispatch determines what changes are allocated to the state. Why not just use string? you could, but when you make a typo, you don't know the error, and you will have a bad weekend again.

Then, we'll add it to the reducer.

const reducer = (state = [], action) => {
  if (action.type === ADD_REQUEST) {
    return [
      {
        id: id(),
        ...action.payload
      },
      ...state
    ];
  }
  return state;
};

That prop drilling isn't great, but we'll deal with it in a bit.

Next improvement is to tell react that if the props is the same as last time, don't change it or update it. How to do this?

Use React.memo (only checks for prop changes) and useCallback (will return a memoized version of the callback that only changes if one of the inputs has changed). This way, not all requests rerendered, only the one that we care about, rendered.

In order to avoid rendering a component with the same props, react.memo should be used to improve performance. useCallback is also a hook that is useful to improve performance, because it returns a memoized version of the callback that only changes if one of the dependencies has changed.

Memoization And Thoughts On State Management Libraries

Let's see how memoization helps.

  • Wrap the action creators in useCallback
  • Wrap NewRequest and Request in React.memo
  • Notice how we can reduce re-renders

Be mindful when using React.memo , use it sparingly because checking the diff of a component itself is work.

And now.. The perils prop drilling.

Prop drilling is the process to get data to parts of the React Component tree. Prop drilling occurs when you have deep component trees, and the Context API provided a way to share values between different components, without having to explicitly pass a prop through every level. The context API allows for better performance.

But, what if the application is getting bigger?

facebook
component tree

It becomes somewhat problematic just for maintainability. What we want is to skip a lot of that, and like say, "Hey, I want to have the availability of any of the state or data anywhere in this tree and they can hook into it".

Before the Context API is getting released, about two years ago there was a legacy Context API and if you went to the documentation, it was like, "Hey, don't use this". And the reason was that the API was not stable. We don't know how this is gonna change, and if you rely on it then bad things could happen if we break it. There were libraries that used it, as React Redux used it, and if you relied on that library and React made the change, and the library could make a change and you would be okay.

But if you did it bespoke in your own application it was a little trickier, right? You were kind of on your own if the API change. So, if you wanted to skip this prop drilling problem, what you had to do is effectively look at a library like MobX + React or React-Redux. And basically opt into this whole other paradigm of matching state. And there were all sorts of really tricky things you could do with hierarchy components and render props, to try to make it work and like in the previous article, that's what we did because none of this existed yet.

And now, there is a kind of public Context API that you can just use that allows you in the same way Hooks gave us reducer patterns out of the box. So the Context API allows you to solve the prop drilling without having to immediately reach for a tool like Redux or Mob x or something along those lines.

There's still a bunch of really valid reasons why you might want to, but the fact that you do not need to and it's a choice that you don't immediately have to make that jump, I think is really important.

Depends on the case, you might consider Mobx or Redux:

  • Redux out of the box supports middleware. You just plug in multiple middleware, but useReducer does not.
  • Redux also gives you combine reducers a whole bunch of other helpers functions. useReducer does not.

So, if you don't need those things, well why would you pull in a library?

There's a lot of things that come for free in Redux so you don't have to implement yourself. For example, we have to implement a very naive version of middleware. React Redux does a lot of performance optimizations that you could do. There is usually a superset of features beyond what you get out of React. But if you don't need them — far before you need them — if you need any of it, you need all of it

flow

Now, there's a lot more you can do in React before you need any of it. We'll see cases where we use a reducer pattern for very simple things that might will definitely not have justified Redux overall. And you might still use those kinds of in that unique component state where you might use Redux for an overall application. For most of the simple cases, you now have it built into the framework. You don't need to necessarily deal with that extra dependency. But if you need all of the extra functionality and features of these more robust solutions, they are still available for you.

In a complex web application, you might use both Redux and the Context API in different places.

Be the first to know when I create something new.
Join my newsletter.