May 29, 2020
0 strokes bestowed

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>
  )
}
Count: 0

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:

Count: 0
Count: 0

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.

Sharing this article on Twitter is a great way to help me out and I really appreciate the support.
+0
Liked the post? Click the beard a few times.
Tags
React
Newer Post: Memoization: What, Why, and How

Let's talk some more about JavaScript, React, and software engineering.

I write a newsletter to share my thoughts and the projects I'm working on. I would love for you to join the conversation. You can unsubscribe at any time.

Data Structures and Algorithms Logo
Data Structures and Algorithms

Check out my courses!

Liked the post? You might like my video courses, too. Click the button to view this course or go to Courses for more information.
Kyle Shevlin's face, which is mostly a beard with eyes
Kyle Shevlin is a front end web developer and software engineer who specializes in JavaScript and React.