State Management with React
Tags: react, tutorial, javascript, webdev
Published: 2022-09-07 14:42:31

## TL;DR

This guide is a compilation of many YouTube videos and articles I watched and read about State management with ReactJS.

State management is very important in React applications because our projects are built around our choices on how to handle them.

Application State can be divided into UI State and Server State (cache). Those are very different from each other; therefore, we shouldn't use the same tools and techniques to handle each one.

Using a Server State management library to handle queries and mutations like React Query or SWR will eliminate most of the complexity in your code, leaving just a few UI states, like open and close modals, to manage with the built-in hooks from React and other techniques.

## State Management with React

> There are various categories of state, but every type of state can fall into one of two buckets:
> 
> 1. Server Cache - State that's actually stored on the server and stored in the client for quick access (like user data).
> 2. UI State - State that's only useful in the UI for controlling the interactive parts of our app (like modal **`isOpen`** state).
> 
> We make a mistake when we combine the two. Server cache has inherently different problems from the UI state and needs to be managed differently. If you embrace that what you have is not actually a state but is instead a ***cache*** of states, then you can start managing it correctly.
> [Kent C. Dodds](https://twitter.com/kentcdodds), [application state management with React](https://kentcdodds.com/blog/application-state-management-with-react).

## UI State Management

React *is* a UI State Management library, and the best approach to handle UI state is using the built-in hook `useState`.

### 1. useState (recommended)

Here's a Counter example to demonstrate some options for handling UI state using React.

```jsx
function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)

  return <button onClick={increment}>{count}</button>
}

function App() {
  return (
    <div>
      <Counter />
    </div>
  )
}
```

#### Shareability

This is a common issue developers face when building React applications. Sharing the state between components is not always easy because components are often too complex.

But let's say your Product Manager wants you to display the current counter value to the user.

```jsx
function CountDisplay() {
  // Where do I get the {count} value?
  return <p>The current counter value is {count}</p>
}

function App() {
  return (
    <div>
      <CountDisplay />
      <Counter />
    </div>
  )
}
```

There's no need for any state management library at this point, we can just share the state between the components via props by **lifting the state** to a common parent of `Counter` and `CountDisplay` (in our case, it's the `App` component).

#### Lifting state

This might seem a little too basic but in the real world is hard to identify these opportunities because our components aren't always as simple as this one.

Here we're moving the state up from the `Counter` component to the `App` component and sharing it via props.

```jsx
function CountDisplay({ count }) {
  return <p>The current counter count is **{count}**</p>
}

function Counter({ count, onIncrement }) {
  return <button onClick={onIncrement}>**{count}**</button>
}

function App() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)

  return (
    <div>
      <CountDisplay />
      <Counter count={count} onIncrement={increment} />
    </div>
  )
}
```

If you want to know more about where to colocate your state, ["Thinking in React"](https://beta.reactjs.org/learn/thinking-in-react) is a good place to start.

#### Prop Drilling

Lifting state is a nice technique to share state with components but when you have to share something from a very high component in the component tree, using that technique will introduce something called [Prop Drilling](https://kentcdodds.com/blog/prop-drilling#what-is-prop-drilling).

For example, imagine that our `CountDisplay` component got too complex for whatever reason, and we decided to move the count message to a child component `CountMessage`.

```jsx
function CountMessage({ count }) {
  return <p>The current counter count is {count}</p>
}

function CountDisplay({ count }) {
  // ...very complex logic
  return <div><CountMessage count={count} /></div>
}

function App() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)

  return (
    <div>
      <CountDisplay />
      <Counter count={count} onIncrement={increment} />
    </div>
  )
}
```

Prop drilling isn't necessarily bad, but using the [Context API to solve this is not recommended](https://reactjs.org/docs/context.html#before-you-use-context).

Instead, we can leverage **component compositions** to avoid this issue.

#### Component Composition

Here's an example that demonstrates how we can avoid prop drilling by using composition:

```jsx
function CountMessage({ count }) {
  return <p>The current counter count is {count}</p>
}

function CountDisplay({ message }) {
  // ...very complex logic
  return <div>{message}</div>
}

function App() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)

  return (
    <div>
      <CountDisplay message={<CountMessage count={count} />} />
      <Counter count={count} onIncrement={increment} />
    </div>
  )
}
```

Passing the `CountMessage` component as property allows us to share the `count` state just one level down the component tree and remove prop drilling.

[Michael Jackson](https://twitter.com/mjackson), the co-founder of [remix_run](https://twitter.com/remix_run), has a wonderful youtube video about [Using Composition in React to Avoid "Prop Drilling"](https://youtu.be/3XaXKiXtNjw).

> **@mjackson:**
> Unpopular opinion: All that junk you're putting in React context should just be props. Legit context uses are rare, mostly for library code, not your app.
> 
> [https://twitter.com/mjackson/status/1195431511417208834](https://twitter.com/mjackson/status/1195431511417208834)

### 2. useContext (not so much recommended)

> **Before You Use Context**
> 
> Context is primarily used when some data needs to be accessible by *many* components at different nesting levels. Apply it sparingly because it makes **component reuse more difficult**.
> 
> **If you only want to avoid passing some props through many levels, the [component composition](https://reactjs.org/docs/composition-vs-inheritance.html) is often a simpler solution than context.**
> 
> [https://reactjs.org/docs/context.html#before-you-use-context](https://reactjs.org/docs/context.html#before-you-use-context)

#### Why is the Context API not so good?

The Context API *is* very powerful, but sometimes we use it for the wrong use cases.

When using it, we need to keep in mind a few things:

- Consumers are bound to their providers. This means you can't reuse consumers (Components that access the Context value) where the provider is unavailable.
- When the provider's value changes, **all** its consumers will re-render. That can lead to unintended performance issues.
- Use it when your provider value doesn't change too often, and you don't mind your consumers re-rendering when they do.

### 3. Other solutions to UI state management

If the React `useState` hook is not enough to handle the UI state in your application, we need to know *why* it wasn't enough and then start looking at different solutions to deal with that use case.

**Jotai** and **Zustand** are good options nowadays, but they solve different problems.

> When to use which:
> 
> - If you need a replacement for useState+useContext, Jotai fits well.
> - If you want to update the state outside React, Zustand works better.
> - If code splitting is important, Jotai should perform well.
> - If you prefer Redux dev tools, Zustand is good to go.
> - If you want to make use of Suspense, Jotai is the one.
> 
> [https://github.com/pmndrs/jotai/issues/13](https://github.com/pmndrs/jotai/issues/13#:~:text=Technical%20difference&text=Zustand%20is%20basically%20a%20single,a%20replacement%20for%20useState%2BuseContext)

**Recoil** is an alternative to Jotai.

If you want to know more, listen to [Theo](https://twitter.com/t3dotgg) talk about [the difference between Jotai and Zustand](https://youtu.be/fa6-Mn0Eybg?t=13394) and how those alternatives differ from Redux.

## Server State Management

Some libraries handle Server states very well and provide you with loading states, query and mutation helpers, optimistic updates with rollbacks and so much more:

1. [React Query](https://tanstack.com/query/v4);
2. [SWR](https://swr.vercel.app/);
3. [Apollo Client](https://www.apollographql.com/docs/react/), only for those using GraphQL;

I haven't used those three enough to tell you which one is better, but here are my considerations about React Query and SWR:

### React Query

- Library with awesome utilities for data fetching, mutation, optimistic updates, and more.
- Built-in developer tools
- Needs a Cache Provider
- More people using it, which means more examples on StackOverflow

#### Basic Example

```jsx
const queryClient = new QueryClient()

function Component() {
  const query = useQuery(['todos'], getTodos);

  return (
    // needs a Cache Provider
    <QueryClientProvider client={queryClient}>
      <TodoList posts={query.data} />
    </QueryClientProvider>
  )
}

function AddTodo() {
  const mutation = useMutation(newTodo => axios.post('/todos', newTodo), {
    onSuccess: () => {
      // needs explicit invalidation
      queryClient.invalidateQueries(['todos']);
    }
  });

  return (
    <button
      onClick={() => {
        mutation.mutate({ id: new Date(), title: 'Do Laundry' })
      }}
    >
      Create Todo
    </button>
  );
}
```

### SWR

- Lightweight library (**9.9kB minified** over 43.1kB minified from react-query) with awesome utilities for data fetching, mutation, optimistic updates, and more.
- [Developer tools with external library](https://swr-devtools.vercel.app/)
- It doesn't need a Cache Provider
- IMO it has a more straightforward implementation

#### Basic Example

```jsx
function Component() {
  const query = useSWR('todos', getTodos);

  return (
    // no Cache Provider or query client
    <TodoList posts={query.data} />
  )
}

function AddTodo() {
  const { mutate } = useSWRConfig();
  // Automatically invalidates queries by key
  const addTodo = (newTodo) => mutate('todos', () => {
    axios.post('/todos', newTodo);
  });

  return (
    <button
      onClick={() => {
        addTodo({ id: new Date(), title: 'Do Laundry' })
      }}
    >
      Create Todo
    </button>
  );
}
```

## Conclusion

If we combine a Server State management library with the techniques mentioned in this article to manage UI State, we can elimate the need to handle server loading states ourselves, when fetching and mutating. Our React projects will become easier to maintain, implement new features, and test them.