The Three Kinds of React State
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>
)
}
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>
)
}
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 RemoteCounter
s 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.