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

Exploring the similarities and differences between useState and useReducer

In this section, we demonstrate some similarities and differences between useState and useReducer.

Implementing useState with useReducer

Implementing useState with useReducer instead is 100% possible. Actually, it's known that useState is implemented with useReducer inside React.

Important Note

This may not hold in the future as useState could be implemented more efficiently.

The following example shows how to implement useState with useReducer:

const useState = (initialState) => {
  const [state, dispatch] = useReducer(
    (prev, action) =>
      typeof action === 'function' ? action(prev) : action,
    initialState
  );
  return [state, dispatch];
};

This can then be simplified and improved upon, as follows:

const reducer = (prev, action) =>
  typeof action === 'function' ? action(prev): prev;
const useState = (initialState) =>
  useReducer(reducer, initialState);

Here, we proved that what you can do with useState can be done with useReducer. So, wherever you have useState, you can just replace it with useReducer.

Implementing useReducer with useState

Now, let's explore if the opposite is possible—can we replace all instances of useReducer with useState? Surprisingly, it's almost true. "Almost" means there are subtle differences. But in general, people expect useReducer to be more flexible than useState, so let's see if useState is flexible enough in reality.

The following example illustrates how to implement the basic capability of useReducer with useState:

const useReducer = (reducer, initialState) => {
  const [state, setState] = useState(initialState);
  const dispatch = (action) =>
    setState(prev => reducer(prev, action));
  return [state, dispatch];
};

In addition to this basic capability, we can implement lazy initialization too. Let's also use useCallback to have a stable dispatch function, as follows:

const useReducer = (reducer, initialArg, init) => {
  const [state, setState] = useState(
    init ? () => init(initialArg) : initialArg,
  );
  const dispatch = useCallback(
    (action) => setState(prev => reducer(prev, action)),
    [reducer]
  );
  return [state, dispatch];
};

This implementation works almost perfectly as a replacement for useReducer. Your use case of useReducer is very likely handled by this implementation.

However, we have two subtle differences. As they are subtle, we don't usually consider them in too much detail. Let's learn about them in the following two subsections to get a deeper understanding.

Using the init function

One difference is that we can define reducer and init outside hooks or components. This is only possible with useReducer and not with useState.

Here is a simple count example:

const init = (count) => ({ count });
const reducer = (prev, delta) => prev + delta;
const ComponentWithUseReducer = ({ initialCount }) => {
  const [state, dispatch] = useReducer(
    reducer,
    initialCount,
    init
  );
  return (
    <div>
      {state}
      <button onClick={() => dispatch(1)}>+1</button>
    </div>
  );
};
const ComponentWithUseState = ({ initialCount }) => {
  const [state, setState] = useState(() => 
    init(initialCount));
  const dispatch = (delta) =>
    setState((prev) => reducer(prev, delta));
  return [state, dispatch];
};

As you can see in ComponentWithUseState, useState requires two inline functions, whereas ComponentWithUseReducer has no inline functions. This is a trivial thing, but some interpreters or compilers can optimize better without inline functions.

Using inline reducers

The inline reducer function can depend on outside variables. This is only possible with useReducer and not with useState. This is a special capability of useReducer.

Important Note

This capability is not usually used and not recommended unless it's really necessary.

Hence, the following code is technically possible:

const useScore = (bonus) =>
  useReducer((prev, delta) => prev + delta + bonus, 0);

This works correctly even when bonus and delta are both updated.

With the useState emulation, this doesn't work correctly. It would use an old bonus value in a previous render. This is because useReducer invokes the reducer function in the render phase.

As noted, this is not typically used, so overall, if we ignore this special behavior, we can say useReducer and useState are basically the same and interchangeable. You could just pick either one, based on your preference or your programming style.