May 29, 2020

The Three Kinds of React State

edit

No matter what it is you’re building with React, when you boil it down, there are only three ways you can manage data in a React app: locally, parentally, and remotely.

Locally

The local management of data happens within the component itself. This is most commonly done with useState, useReducer, or if using a class component, this.state. The data and its updaters are encapsulated within the scope of the component. It’s simple and overplayed, but a Counter component can be a useful example here.

function Counter({ initialValue = 0 }) {
  const [count, setCount] = React.useState(initialValue)

  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(initialValue)

  return (
    <div>
      <div>Count: {count}</div>
      <div>
        <button type="button" onClick={increment}>
          +1
        </button>
        <button type="button" onClick={decrement}>
          -1
        </button>
        <button type="button" onClick={reset}>
          reset
        </button>
      </div>
    </div>
  )
}
Count: 0

Parentally

The parental management of data happens when the data and its updaters are passed in as props from somewhere higher in the component tree. In the case of our counter, it is as simple as moving the data and updating functions to a parent controlling component, and passing them in as props to a controlled child component.

function CounterController({ initialValue = 0 }) {
  const [count, setCount] = React.useState(initialValue)

  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(initialValue)

  return (
    <Counter
      count={count}
      increment={increment}
      decrement={decrement}
      reset={reset}
    />
  )
}

function Counter({ count, increment, decrement, reset }) {
  return (
    <div>
      <div>Count: {count}</div>
      <div>
        <button type="button" onClick={increment}>
          +1
        </button>
        <button type="button" onClick={decrement}>
          -1
        </button>
        <button type="button" onClick={reset}>
          reset
        </button>
      </div>
    </div>
  )
}
Count: 0

This component behaves exactly the same as the other one, but there might be a good reason to store the data in a parent component. Perhaps it needs to be accessed by other children, or it’s easier to test the child component by being able to pass props into it rather than assert on its internal behavior. The reason for the separation of the data and its management from its presentation isn’t terribly important at this moment. Understanding the concepts of this pattern is what matters.

Remotely

The remote management of data happens when we store and update data outside of the ancestry of a component tree. This is a very broad area of data management with many solutions. Common ones are Redux or the React Context API. The important part of this concept is that the data and its updaters are at a distance from the consuming component and require some mechanism for supplying the data to the consumer.

I’m going to use React’s Context API to demonstrate. We’re going to create a CounterContext and consume it with our Counter component.

const CounterContext = React.createContext()

function CounterProvider({ children, initialValue = 0 }) {
  const [count, setCount] = React.useState(initialValue)

  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(initialValue)

  return (
    <CounterContext.Provider
      value={{
        count,
        decrement,
        increment,
        reset,
      }}
    >
      {children}
    </CounterContext.Provider>
  )
}

const useCounterContext = () => React.useContext(CounterContext)

function RemoteCounter() {
  const { count, decrement, increment, reset } = useCounterContext()

  return (
    <div>
      <div>Count: {count}</div>
      <div>
        <button type="button" onClick={increment}>
          +1
        </button>
        <button type="button" onClick={decrement}>
          -1
        </button>
        <button type="button" onClick={reset}>
          reset
        </button>
      </div>
    </div>
  )
}

function App() {
  return (
    <CounterProvider>
      <RemoteCounter />
    </CounterProvider>
  )
}

What is interesting about our RemoteCounter example is that because it consumes data managed remotely, instances of RemoteCounter within the same CounterProvider read and update the same data. Here are two RemoteCounters within a single CounterProvider:

We could accomplish something similar having components read and write to a global store in our application. Again, how data is managed remotely is not what’s important here, understanding the pattern is what matters.

Conclusion

It doesn’t matter how complicated an application you are building. There are still only three patterns for data management in a React application. Master these patterns. You will use them over and over and over again.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Just Enough Functional Programming
Just Enough Functional Programming
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.