this.setState()
, this.state
, useState()
, useReducer()
) and what might lead a developer to augment it with a third-party state manager (Flux > Redux, Mobx etc.) or replace it completely.
This will briefly outline how local state is used within a React component.
React component state (aka local state) is ideally encapsulated to a
component and typically represents anything about the UI that
changes over time. When building a basic counter UI the only part of
the UI that changes is the count. Thus, if we were to build a
Counter
component, minimally, the component would have a count
state (i.e., the current numeric number representing a count).
This is an example of a Counter
component written as a React class component using a count state:
import React, { Component } from "react";
export default class Counter extends Component {
state = {
count: 0
};
handleIncrement = () => {
this.setState({
count: this.state.count + 1
});
};
handleDecrement = () => {
this.setState({
count: this.state.count - 1
});
};
reset = () => {
this.setState({
count: 0
});
};
render() {
const { count } = this.state;
return (
<section className="Counter">
<h1>Count: {count}</h1>
<button onClick={this.handleIncrement} className="full-width">
Increment
</button>
<button onClick={this.handleDecrement} className="full-width">
Decrement
</button>
<button onClick={this.reset} className="full-width">
Reset
</button>
</section>
);
}
}
As of React 16.8 a functional component needing state can also be written using React Hooks and does not require the use of JavaScript class syntax.
The example below is the Counter
component above re-written as a React function component using a count state via React Hooks:
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1);
};
const handleDecrement = () => {
setCount(count - 1);
};
const reset = () => {
setCount(0);
};
return (
<section className="Counter">
<h1>Count: {count}</h1>
<button onClick={handleIncrement} className="full-width">
Increment
</button>
<button onClick={handleDecrement} className="full-width">
Decrement
</button>
<button onClick={reset} className="full-width">
Reset
</button>
</section>
);
}
export default Counter;
Keep In Mind:
count
).props
(i.e. lifting state)These notes discuss taking state and moving it up in the component tree to then pass it from an ancestral component back down to child components (i.e. lifting state using the React Container/Smart/Controller Component Pattern).
Adding state to a component isn't a complicated or boilerplate filled exercise. However, React has this notation of passing state down to components using props
. And this practice is a massive slippery slope when dealing with large component trees that have to scale. Encapsulating state to the inside of a component, while advertised as the ideal, is pragmatically never the reality when dealing with a complex web application. It seems to me that an app doesn't have to grow that big before one has to start lifting the state up, only to pass it back down via props
. The idea that one can keep scope encapsulate is a nice idea, and when you can do it, you should, but this isn't solving boilerplate state management issues in large React applications. The state has to get shared and sharing is difficult.
Taking the Counter
component introduced in section 1 of these notes, and the idea of lifting state, the counter code can be re-written so that the state is lifted up to a single ancestral component (i.e. CounterContainer
). The React recommend way to write this component, taking full advantage of reusable "dumb" components (aka presentational components), would be to lift state up to a common "smart" container component (can't find one, then create a container component) where state can be stored in a ancestral/parent component and sent down to dumb/presentational components. Here is an example of such a re-write:
Sharing state with child components isn't that complicated. All one is doing is passing JavaScript references downward. And when communicating these ideas with a couple of components in the context of a simple counter UI, the concept of lifting state can come across as rather straight forward.
However, what happens when one is not dealing with a couple of components but thousands? What happens if we need state available to all components, and the topmost component in the component tree is the only option? What if our application is filled with state, and we have layers and layers of smart container components spread all over the tree requiring the passing of state from not just one or two components but through potentially 5 or 10 components (i.e., prop drilling)?
The application below demonstrates a slightly larger than Counter
example of a growing a React application with lifted state. Study the application's usage of state. Specifically, where the state is located and how one has to manually keep moving state downwards to children at different points in the component tree to keep it as relative as possible to the components that use it (i.e., can't keep all local state at the top can we? Wouldn't that make it relative to all components in the tree?).
After examining the packing apps usage of state imagine now only using component state for an application with thousands of components with different types of state (i.e. global v.s. relative) living different life spans (i.e. non-lasting state, short-lasting state, long-lasting state).
Keep In Mind:
As a component tree grows, lifting state comes at a cost. Especially if the state has to be at the top of the component tree. The cost is often called, "prop drilling".
If you want to share state between components in the component tree, you will have to lift state up to a common ancestral components and then pass that state and functions that update state back down via props. If you need sharable global state you will have to lift it to the top of the tree. In either case you have a prop drilling problem unfolding.
It is at this point you either live with prop drilling hell (some claim its explicitness and manually nature resulting in boilerplate hell is a feature) or try and use the React context API to move data through the component tree without having to pass down props manually at every level.
As an example, I've re-written the previous CounterContainer
component to use a CounterStateContext
component (i.e. <CounterStateContext.Provider>
and <CounterStateContext.Consumer>
) to pass state from the Counter
to sub components avoiding prop drilling on the RandomComponentX
and RandomComponentY
components.
Below I've re-written the above context API example to use React hooks for state and the useContext()
hook instead of a consumer component (i.e. not this <CounterStateContext.Consumer>
, but this: const CounterState = React.useContext(CounterStateContext);
):
Keep In Mind:
Outlines how components can re-use state so one is not repeating themselves.
React local state (as well as any value really) can be re-used using the following three component patterns in React:
Below the CounterContainer
example is re-written for each pattern so that the state is not repeated for a second counter.
Keep In Mind:
useReducer()
for complex state
Outlines the use of a Redux-like pattern for managing state using the React userReducer()
React Hook instead of this.setState()
and this.state
.
The useState()
does a good job managing primitive values (e.g. string, integer, boolean) but when you want to encapsulate several values in state and need to use an Array or Object the useReducer()
hook will likely be a better solution.
In the code example below the counter code, that uses React Hooks, has been re-written to make use useReducer()
instead of useState()
.
import React, { useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "COUNT":
return { count: state.count + action.by };
case "RESET":
return { count: state.count - state.count };
default:
throw new Error();
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, { count: 0 });
const handleIncrement = () => {
dispatch({ type: "COUNT", by: 1 });
};
const handleDecrement = () => {
dispatch({ type: "COUNT", by: -1 });
};
const reset = () => {
dispatch({ type: "RESET" });
};
return (
<section className="Counter">
<h1>Count: {state.count}</h1>
<button onClick={handleIncrement} className="full-width">
Increment
</button>
<button onClick={handleDecrement} className="full-width">
Decrement
</button>
<button onClick={reset} className="full-width">
Reset
</button>
</section>
);
};
export default Counter;
Keep In Mind:
This section will discuss how to handle values that are derived from locale state but are not state themselves (e.g. first name + last name = full name, full name is derived not state itself)
Javascript offers the get
syntax which is perfect to derive a value from actual component state. In the example below the full name is being derived from the first and last name local component state.
class FirstNameClass extends Component {
state = {
firstName: "Cody",
lastName: "Sooner"
};
newNames = () => {
this.setState({
firstName: "Taco",
lastName: "Johns"
});
};
get fullname() {
return `${this.state.firstName} ${this.state.lastName}`;
}
render() {
return (
<>
{this.fullname}
<br />
<button onClick={this.newNames}>New Name</button>
</>
);
}
}
Here is a React Hooks version:
const Counter = () => {
const [firstName, setFirstName] = useState("Pat");
const [lastName, setlastName] = useState("Doe");
const newNames = () => {
setFirstName("Lee");
setlastName("Homer");
};
const getComputed = {
get fullname() {
return `${firstName} ${lastName}`;
}
};
return (
<>
{getComputed.fullname}
<br />
<button onClick={newNames}>newNames</button>
</>
);
};
This section briefly outlines why someone would choose to manage state externally from the component tree.
You might wonder if what React offers alone is now enough to avoid external state managers like Redux or Mobx. So you might need Redux or Mobx if:
useReducer()
currently creates a unique dispatch function each time).These external resources have been used in the creation of these notes.