Managing State in React

Mark Ezema

Mark Ezema / April 08, 2022

5 min read

Managing Application State

Managing application state is like trying to build a giant jigsaw puzzle without having the picture on the box. It’s why there exists a multitude of state management libraries that make the difficult task easier. Despite the difficulty of the problem, we often tend to overcomplicate the solution.

React components are like building blocks that help us construct our applications, and state management should be no different. I’ve found success in thinking of the application state as a tree structure that maps directly to the application’s architecture.

Redux

Redux was so successful because it solved the dreaded prop drilling problem. It allowed developers to share data across different parts of the tree by simply passing components into a magical connect function. This is great, but it can lead to problems such as having to constantly interact with reducers, action creators, and dispatch calls. This can become increasingly difficult the larger the application gets.

It’s also important to remember that we don’t need to throw all of our state into redux. For simpler state, such as whether a modal is open or what a form’s input value is, putting it into Redux can lead to performance issues. It’s better to break up the state and locate it closer to where it matters so that we don’t have these problems.

Lifting State Up

Take the example of a counter. To manage the count, you can use the React state hook to create a local state for the component:

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return <button onClick={increment}>{count}</button>
}

function App() {
  return <Counter />
}

But what do you do when you need to share the state across components? You apply “Lifting State Up.” This is the answer to the state management problem in React and it’s like taking all of the pieces of the puzzle and fitting them together. Here’s how you would apply it to the example:

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

function CountDisplay({count}) {
  return <div>The current counter count is {count}</div>
}

function App() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <CountDisplay count={count} />
      <Counter count={count} onIncrementClick={increment} />
    </div>
  )
}

Now, you no longer need to reach for a state management library, like react-redux, because you have the pieces of the puzzle already. Just keep lifting the state all the way up the app.

Component Composition

But, what about the prop drilling problem? To avoid this, you can use component composition. This is like taking the puzzle pieces and making sure they all fit together in the right way. For example, you could have:

function App() {
  const [someState, setSomeState] = React.useState('some state')
  return (
    <>
      <Header 
        logo={<Logo someState={someState} />}
        settings={<Settings onStateChange={setSomeState} />}
      />
      <LeftNav>
        <SomeLink someState={someState} />
        <SomeOtherLink someState={someState} />
        <Etc someState={someState} />
      </LeftNav>
      <MainContent>
        <SomeSensibleComponent someState={someState} />
        <AndSoOn someState={someState} />
      </MainContent>
    </>
  )
}

React Context API

But, eventually, even composition won’t be enough and you’ll need to reach for React’s Context API. This is like having a toolbox of pieces that make up your puzzle. It’s officially supported by React now, so you can use it without any problems.

You can also put all the logic for common ways to update the state in your useCount hook. This gives you flexibility and reduces complexity. You should remember to not make everything in your application part of one state object, keep state as close to where it’s needed as possible, and to create multiple providers.

Performance

When it comes to performance, the first thing to do is check whether components are being re-rendered unnecessarily. If they are, then it's time to look at speeding up your renders. However, if the bottleneck lies in the state management, then it's time to look at separating state into different logical pieces, optimizing your context provider, or bringing in jotai.

At the end of the day, React's built-in state management abstractions are not always the best solution. In those cases, libraries like recoil and jotai can be incredibly helpful.

Conclusion

In short, local state should be kept as local as possible and context should only be used when prop drilling becomes a problem. Following these steps will make it easier to maintain state interactions. Server Cache vs UI State can be thought of like a house. Server Cache is the foundation of the house, as it stores the data that is necessary for the house to function. UI State is like the walls and windows of the house - it's the interactive parts that are necessary for the house to be functional and comfortable.

React is like a puzzle and by using the right pieces, you can build something amazing.

Subscribe to the newsletter

Get emails from me about updates in the world of Artificial Intelligence, and Personal Development.

View all issues