Book Image

Micro State Management with React Hooks

By : Daishi Kato
Book Image

Micro State Management with React Hooks

By: Daishi Kato

Overview of this book

State management is one of the most complex concepts in React. Traditionally, developers have used monolithic state management solutions. Thanks to React Hooks, micro state management is something tuned for moving your application from a monolith to a microservice. This book provides a hands-on approach to the implementation of micro state management that will have you up and running and productive in no time. You’ll learn basic patterns for state management in React and understand how to overcome the challenges encountered when you need to make the state global. Later chapters will show you how slicing a state into pieces is the way to overcome limitations. Using hooks, you'll see how you can easily reuse logic and have several solutions for specific domains, such as form state and server cache state. Finally, you'll explore how to use libraries such as Zustand, Jotai, and Valtio to organize state and manage development efficiently. By the end of this React book, you'll have learned how to choose the right global state management solution for your app requirement.
Table of Contents (16 chapters)
1
Part 1: React Hooks and Micro State Management
3
Part 2: Basic Approaches to the Global State
8
Part 3: Library Implementations and Their Uses

Working with useState

In this section, we will learn how to use useState, from basic usage to advanced usage. We start with the simplest form, which is updating with the state with a new value, then updating with a function, which is a very powerful feature, and finally, we will discuss lazy initialization.

Updating the state value with a value

One way to update the state value with useState is by providing a new value. You can pass a new value to the function returned by useState that will eventually replace the state value with the new value.

Here is a counter example showing updating with a value:

const Component = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      {count}
      <button onClick={() => setCount(1)}>
        Set Count to 1
      </button>
    </div>
  );
};

You pass a value of 1 to setCount in the onClick handler. If you click the button, it will trigger Component to re-render with count=1.

What would happen if you clicked the button again? It will invoke setCount(1) again, but as it is the same value, it "bails out" and the component won't re-render. Bailout is a technical term in React and basically means avoiding triggering re-renders.

Let's look at another example here:

const Component = () => {
  const [state, setState] = useState({ count: 0 });
  return (
    <div>
      {state.count}
      <button onClick={() => setState({ count: 1 })}>
        Set Count to 1
      </button>
    </div>
  );
};

This behaves exactly the same as the previous example for the first click; however, if you click the button again, the component will re-render. You don't see any difference on screen because the count hasn't changed. This happens because the second click creates a new object, { count: 1 }, and it's different from the previous object.

Now, this leads to the following bad practice:

const Component = () => {
  const [state, setState] = useState({ count: 0 });
  return (
    <div>
      {state.count}
      <button
        onClick={() => { state.count = 1; setState(state); }
      >
        Set Count to 1
      </button>
    </div>
  );
};

This doesn't work as expected. Even if you click the button, it won't re-render. This is because the state object is referentially unchanged, and it bails out, meaning this alone doesn't trigger the re-render.

Finally, there's an interesting usage of value update, which we can see here:

const Component = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>
        Set Count to {count + 1}
      </button>
    </div>
  );
};

Clicking the button will increment the count; however, if you click the button twice quickly enough, it will increment by just one number. This is sometimes desirable as it matches with the button title, but sometimes it's not if you expect to count how many times the button is actually clicked. That requires a function update.

Updating the state value with a function

Another way to update the state with useState is called a function update.

Here is a counter example showing updating with a function:

const Component = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      {count}
      <button onClick={() => setCount((c) => c + 1)}>
        Increment Count
      </button>
    </div>
  );
};

This actually counts how many times the button is clicked, because (c) => c + 1 is invoked sequentially. As we saw in the previous section, value update has the same use case as the Set Count to {count + 1} feature. In most use cases, function updates work better if the update is based on the previous value. The Set Count to {count + 1} feature actually means that it doesn't depend on the previous value but depends on the displayed value.

Bailout is also possible with function updates. Here's an example to demonstrate this:

const Component = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(
      () => setCount((c) => c + 1),
      1000,
    );
    return () => clearInterval(id);
  }, []);
  return (
    <div>
      {count}
      <button
        onClick={() =>
          setCount((c) => c % 2 === 0 ? c : c + 1)}
      >
        Increment Count if it makes the result even
      </button>
    </div>
  );
};

If the update function returns the exact same state as the previous state, it will bail out, and this component won't re-render. For example, if you invoke setCount((c) => c), it will never re-render.

Lazy initialization

useState can receive a function for initialization that will be evaluated only in the first render. We can do something like this:

const init = () => 0;
const Component = () => {
  const [count, setCount] = useState(init);
  return (
    <div>
      {count}
      <button onClick={() => setCount((c) => c + 1)}>
        Increment Count
      </button>
    </div>
  );
};

The use of init in this example is not very effective because returning 0 doesn't require much computation, but the point is that the init function can include heavy computation and is only invoked to get the initial state. The init function is evaluated lazily, not evaluated before calling useState; in other words, it's invoked just once on mount.

We have now learned how to use useState; next up is useReducer.