The most complete guide to React State you’ll ever read

The World of React + React Native State 🌍

Kush Tran
Kush Tran
Jan 11 · 17 min read
Image for post
Image for post

When i first learned about State, it is so friendly and simply just “data that is going to change + mutating it triggers a component rendering”. That’s what people told me.

After years working with it, i started perceiving the gap in my knowledge. The State I used to know, comes with more concepts & related concerns that i only realized after hitting hardship & lots of researching effort. Hardly had i found a post covering them all. So in this blog post, let’s break it down !

Table of contents:

The Problem

Props are great materials for customizing components. But everyone knows it is immutable, can’t be mutated, which prevents the flexibility because in real world example, a component sometimes need to change over time.

State is a part of a component born to store data that gonna changes, and component reacts to those changes.

For example, water boils at 100C, and freezes at 0C. The temperature changes that decides the water’s state. The temperature here is like a State, and the water is like a component, this component reacts to the change of State.

You probably don’t need State?

Image for post
Image for post

In case of mutate data, lots of people struggle with deciding when to use State, when not to use. If you haven’t known yet, State relates closely to triggering a re-render, and storing data into State unnecessarily triggers “wasted renders”.

I divide the usage of mutate data into two category:

And the rule always is: Only use state to store mutate data that relate to rendering.

Because with data not relating to render, we don’t need to display it to the UI tree, so there no need to queue a re-render process, calculate the UI tree.

Otherwise, when it relates to render, mutating the state data queues a re-render process, which outputs the new tree UI and React will diff that tree to collect changes, then update to the real tree.

Let’s break down a simple example.

Image for post
Image for post

In above example, saying i have three buttons, two of which display the number of counts, including one will display double if isDouble is true.

The third button shows whether isDouble is ON or OFF.

Every time i press on any of the two buttons, the count value then increase by 1, and btnPressed records the position of the button just pressed. And pressing on the third button will toggle isDouble between ON or OFF.

Count is used for displaying, so it’s related to rendering → store to State.

isDouble is not used for displaying, but it decides the display value: ON or OFF, and decide whether count should be doubled, so it’s related to rendering → store to State.

btnPressed is just a normal data storing position, it’t neither displayed or deciding any displaying, so it’s not related to rendering → do not store to State.

With data like btnPressed:

For class component, you can use instance variables to store it

For function component, you can either use outside function variables or useRef to store it. The difference between them can motivate to write a separate blog post, so i won’t go into detail here, you can search it online.

Okay, one more time, with me

Only use state to store mutate data that relate to rendering.

Image for post
Image for post

The big rendering picture (Bonus information, you can skip if you aren’t interested)

Image for post
Image for post
This is the current life cycles at the time this post written

React has lots of different core concepts. Altogether, they form a big picture of rendering. Those concepts relate closely to each other.

Today post is about State, so let’s see how State is a part of the big picture through three main phase: (not actual phases of rendering process, just my naming lol )

— — — Class Component — — —

The initial phase:

Image for post
Image for post

State is an asset of Component, when it comes to initial our State, we can either declare it inside constructor or make use of getInitialState.

// ES6class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
}
// ...
}
// ES5const Counter = createReactClass({
getInitialState: function() {
return {count: 0};
},
// ...
});

The only difference between them is that getInitialState+createReactClass is the old legacy and constructor is the new way to initial state. React allow us do something like this:

class Counter extends React.Component {
this.state = {
count: 0
};
// ...
}

It’s eventually put into constructor so it’s the same.

After state has been initialed. The process moves on to getDerivedStateFromProps.

getDerivedStateFromProps exists for only one purpose. It enables a component to update its internal state as the result of changes in props.(I’ll write in detail about this case in upcoming blog about The anti-pattern Derived State).

Then render() goes into action, running JSX to create ReactNode/ReactElement, forming out the first shape of our UI tree.

And componentDidMount comes behind that, at this point, React has updated refs and UI tree. This is important, because we might mistake using refs before this life cycles → resulting in undefined/null refs.

On more interesting note about componentDidMount, it’s where we handle Side Effects like calling to APIs. Why is that?

async componentWillMount(){
const response = await API.fetchList()
if(response.status == 200){
this.setState({listData: response.data})
} else {
this.setState({error: response.error})
}
}

The reason is to avoid undefined default state. Saying we fetch data before that, in the old componentWillMount(deprecated). Fetching data most of the time, running asynchronous, so it’s no guarantee it will finish before render()

And we use this.state.listData inside render(). And some silly time, we forget to initial default value of State → CRASH. So fetching in componentDidMount, like a reminder for us to initial default state value.

The update phase:

Image for post
Image for post

setState queues an update/rendering on Component, it’s a way to inform the component to start the update process. setState will trigger an update no matter the new state value has changed or not. While setter of useState checks this - using shallow equality for object.

shouldComponentUpdate is a built-in life cycle provided as a way to optimize rendering (skip rendering) if return false, and for we not always need to optimize, its default behavior is return true.

getSnapshotBeforeUpdate is invoked right before the most recently rendered output is committed to the real tree. It provide us access to prevProps and prevState for any special calculation to capture the render nodes before it is commited to the real tree, and the return value is passed to componentDidUpdate. Anyway, still not a common used life cycle

componentDidUpdate is invoked right after the updating, not happening when first mounting component. Use this as an opportunity to handle side effects relating to the changing of new props & state. If your component implements the getSnapshotBeforeUpdate() lifecycle (which is rare), the value it returns will be passed as a third “snapshot” parameter to componentDidUpdate(). Otherwise this parameter will be undefined.

The destroy phase:

The name is already obvious guys ^^

— — — Function Component — — —

Since the release of Hook in React 16.8. It provides the capability of State for Function Component with the hook useState. Hooray !

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

With the power of Hook, personally, I don’t care about life cycles anymore, because reducing the amount of definitions/concepts is one of the reasons of Hook.

Initial Phase

Different from Class, the nature of Function is captured value. So when a render happens, it re-run the function as well as redraw UI with its current version of data. Hook is likely to act as normal object having its own version for each renders.

const hook: Hook = {   
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};

useState is just one of the built-in Hooks. Basically they share the same object structure like above.

React will start mountState . Basic ideal is to mount a hook through mountWorkInProgressHook(), and it starts off like

{   
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
}

at the end hook become: (example with initial state = 0 )

{
memoizedState: 0, // our initial state
baseState: 0, // our initial state
queue: {
last: null,
dispatch: dispatchAction.bind(null, currentlyRenderingFiber, queue),
lastRenderedReducer: basicStateReducer(state, action),
lastRenderedState: 0, // our initial state
},
baseUpdate: null,
next: null,
}

[hook.memoizedState, dispatch] are prepared to return for

[count, setCount] = useState(0)

React exposes hooks to us as functions but under the hood, they are modelled as objects.

Here is the detail of mountState:

function mountState<S>(  initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] { 
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<BasicStateAction<S>, > = (
queue.dispatch = (dispatchAction.bind(null,currentlyRenderingFiber,queue): any)
);
return [hook.memoizedState, dispatch];
}

Update Phase

Every time setCount (example above) triggers our component to re-render, the Hook memoize the new state, that’s way the fresh render updated with new Hook value.

Firstly, have a look again at our current hook value:

{
memoizedState: 0, // our initial state
baseState: 0, // our initial state
queue: {
last: null,
dispatch: dispatchAction.bind(null, currentlyRenderingFiber, queue),
lastRenderedReducer: basicStateReducer(state, action),
lastRenderedState: 0, // our initial state
},
baseUpdate: null,
next: null,
}

dispatchAction.bind(null, currentlyRenderingFiber, queue) → currentlyRenderingFiber.

Fiber is an object that is mutable, holds component state and represents the tree. React generate a tree of these objects, that’s how it models the entire component tree. I call it the representative of a component. Each component has its own Fiber. Fiber is used to identify component.

Fiber has a property named memoizedState. Which stores the state of our Counter component. Its value is the hook object we created at mountState above, with the next and baseUpdate properties are null.

For example, if our Component got more hooks :

function Counter () {
const [count, setCount] = useState(0);
const [check, setIsCheck] = useState(true);
...
}

our memoizedState of Fiber will be:

{
memoizedState: 0, // the setCount hook
baseState: 0,
queue: { /* ... */},
baseUpdate: null,
next: { // the setIsCheck hook
memoizedState: true,
baseState: true,
queue: { /* ... */},
baseUpdate: null,
next: null
}
}

Yeah hooks of a component is implementation of LinkedList.

On mounting, an array of HookTypesDev created. It’s used to identify the order of hooks. Order is important, so there are rules of hooks prevent you breaking its implementation.

['useState', 'useState',...] // array of order hook types

For example:

function Counter () {
const [count, setCount] = useState(0);
const [anotherCount, setAnotherCount] = useState(0);
const [check, setIsCheck] = useState(true);
useEffect(()=>{
console.log("hello world")
},[])
...
}hooktypes will be
==> ['useState', 'useState', 'useState', 'useEffect']

The idea mountHookTypesDev():

const hookName = ((currentHookNameInDev: any): HookType);    
if (hookTypesDev === null) {
hookTypesDev = [hookName];
} else {
hookTypesDev.push(hookName);
}

HookTypes is stored at _debugHookTypes property inside Fiber.

// Used to verify that the order of hooks does not change between renders.  
_debugHookTypes?: Array<HookType> | null,

When running setCount to trigger updating, updateHookTypesDev() is called and it uses index to check whether the value “hookName” of HookTypesDev is equal to currentHookNameInDev. Guarantee the order of LinkedList hooks, so this approach help update the right hook.

const hookName = ((currentHookNameInDev: any): HookType); 
if (hookTypesDev !== null) {
hookTypesUpdateIndexDev++;
if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) {
warnOnHookMismatchInDev(hookName);
}
}

After setCount to 1, the end result would be

{
memoizedState: 1, // our new state
baseState: 1, // our new state
queue: {
last: {
expirationTime: 1073741823,
suspenseConfig: null,
action: 1,
eagerReducer: basicStateReducer(state, action),
eagerState: 1, // our new state
next: { /* ... */},
priority: 98
},
dispatch: dispatchAction.bind(null, currentlyRenderingFiber, queue),
lastRenderedReducer: basicStateReducer(state, action),
lastRenderedState: 1, // our new state
},
baseUpdate: {
expirationTime: 1073741823,
suspenseConfig: null,
action: 1,
eagerReducer: basicStateReducer(state, action),
eagerState: 1, // our new state
next: { /* ... */},
priority: 98
},
next: null,
}

A lot has changed. The most significant being queue.last and baseUpdate. They contain information about the action just updated.

I won’t diving into any deeper, and stop here. My mission is to introduce to you the main basic idea how hook implemented. For more information, you can find it here. Now let’s continue.

Why Asynchronous?

You probably know that updating state works asynchronously, which means the updated value is not presented immediately.

Image for post
Image for post

For example, logging the count value right after the setCount call, return the old value 0, which we aren’t expecting. Sometimes i found newbies posting issues onto groups asking why their value still the same.

Because of Asynchronous.

Let’s see why they design it that way.

1. Batching Update

When they design the API, the React team know that, in real world work, there are cases requiring us to trigger multiple updates at once. And Imagine how running the multiple update processes consecutively can lead to ugly performance issue. Render batching is when multiple calls to setState()/useState setterresult in a single render pass being queued and executed.

Let’s saying i press on my button, it increases count three times, we expect the end count value = 3. Unfortunately, because updating state works async, so the count value was equal 0 and it remains throughout the function, so no matter how many time setCount executes, the execution always is 0 + 1. That’s why count = 1. This is a case happening when you calculate based on previous state, be careful !

For the setIsCheck, it’s not calculating based on prevState, so it works like we expected, the end value is true.

Still, it only queues one render pass ===> batching update. You can test it by logging inside useEffect

const [count, setCount] = useState(0)
const [isCheck, setIsCheck] = useState(false)
const onPress = () => {
setCount(count + 1) // 0+1
setCount(count + 1) // 0+1
setCount(count + 1) // 0+1
setIsCheck(true)
setIsCheck(false)
setIsCheck(true)
//evaluate to
this.setState({count: this.state.count +1})
this.setState({count: this.state.count +1})
this.setState({count: this.state.count +1})
// the final result: count = 1, isCheck = true
}

The API gives us access to the previous value of State, so we can calculate new value based on old value. This approach expose the value of state ahead of rendering process, and using the value to calculation is sweet.

const [count, setCount] = useState(0)
const [isCheck, setIsCheck] = useState(false)
const onPress = () => {
setCount(prevCount => prevCount + 1) // 0+1
setCount(prevCount => prevCount + 1) // 1+1
setCount(prevCount => prevCount + 1) // 2+1
setIsCheck(true)
setIsCheck(false)
setIsCheck(true)
//evaluate to
this.setState(prevState => {count: prevState.count +1})
this.setState(prevState => {count: prevState.count +1})
this.setState(prevState => {count: prevState.count +1})
// the final result: count = 3, isCheck = true
}

Don’t break batch update

It’s important to note that React will only batch updates that occurs in event handlers. This means that any state updates queued outside of the actual immediate call stack will not be batched together.

const [counter, setCounter] = useState(0)

const onPress = async () => {
setCount(0)
setCount(1) // 1st render pass

const data = await fetchSomeData()

setCount(2) // 2nd render pass
setCount(3) // 3rd render pass
}

This will be 3 render passes.

The first render pass is a batch for setCount(0) and setCount(1) because they’re called in the same event handler

The second render pass happens for setCount(2), because the synchronous of the original event has finished, and after await for fetchData, React begins a new call.

Then third render pass happens the same thing for setCount(3), because it’s also running outside original event, so out batching

This case happens a lot, and not totally bad.Furthermore, I have seen lots of folks doing something like await this.setState() to wait for state update and get the current value, that breaks the batching feature as well. In other words, they unintentionally breaks batch update due to wait for the state value, and use it for the next calculation. This can be considering Handling Side Effect (we learn it in the next section).

2. Guaranteeing Internal Consistency

Even if state is updated synchronously, props are not. You can’t know props until you re-render the parent component, and if you do this synchronously, batching makes no good use.

==> state, props, refs are internal consistent inside React, it’s easy to see how that is through usage in React core concepts.

Let’s saying we allow to update state synchronously. This works !

console.log(count) // 0
setCount(count + 1)
console.log(count) // 1
setCount(count + 1)
console.log(count) // 2

However, let’s say, one day other child components need to make use of count, so you have to refactor and lift state up to your parent component to share your state. This is common.

props.onIncrement() // does the same thing - increase count + 1

and this breaks

console.log(props.count) // 0
props.onIncrement();
console.log(props.count) // 0
props.onIncrement();
console.log(props.count) // 0

Because if state works synchronous, state is flushed immediately, but props.count is not. We can’t flush props immediately without re-rendering parent component, if we do so, it’s breaking batching.

Side Effects

Side effects are effects/works happens after we have changed something or it’s not pure function.

Image for post
Image for post

Working with state is likely to always thinking about side effects.

Asking yourself this question:

After this state changed, what happens next?

Does your text go from black to red?

Does your name go from Kush to Elsa?

Does you have to fetch another data based on new state?

What gonna happen next bro?

A state changes lead to some other changes

If your states change without any thing happens, you might not need to use state. Instead, you can use other variable kind to store your data, because it’s no need trigger a render pass.

Let’s saying i have a state storing the city name, and a Text display it. With that city name, i fetch data about streets in that city. So my city state is used for displaying and fetching data. I don’t want to split it into normal variable for fetch streets data, because i want single of truth. In this context, fetching data is a side effect, it happens every time i change city state.

React provides some ways to handle side effect happening according to state change, allow you to access fresh state value.

Class component

setState comes with a callback as 2nd argument. It’s a place we know our state is done updating. So we can access the fresh state value without committing any bad practice like await breaking batch update (mentioned above).

this.setState(
{ city: newCity },
()=> this.fetchStreets(this.state.city)
)

2. componentDidUpdate():

This lifecycle can handle side effect as well. But be careful for infinite updating. Commonly have to use with comparison.

Function Component

useEffect with 2nd argument for array dependencies is great for handling all kind of side effects.

const [city, setCity] = useState("")useEffect(()=>(
fetchStreets(city)
), [city])

The effect happens every time the citychanges, including the first initial value of empty. I love this hook more than the class’s approaches, because i can code without worrying about forget to run fetchStreets() every time i change city, while approaches from class have to call it in the first rendering in componentDidMount() and manage later changing city. And it divide my logic into isolated blocks, easy to read, every effect relates to city assemble at one place.

Hook & function component for better ^^

2. useLayoutEffect:

This is identical to useEffect, but it fires synchronously after all DOM/ShadowNodes mutations. Use this to read layout from the DOM/ShadowNodes and synchronously re-render.

useEffect is the recommended way.

Placing your State

Sometimes deciding where to put state is crucial to boost our performance. Inside a screen layout, with full of parent components, and child components. It’s not easy to decide the location for your state

So the question is:

What factor decides the state position?

Image for post
Image for post

The only answer is the state’s purpose.

I mean, what will the state be used for?

Is it used for only this component? Yes, Colocate State

Is it used for many components (siblings/parents)? Yes, Lift State

Image for post
Image for post
Image for post
Image for post

How Colocate State Boosts Performance

Image for post
Image for post

Let’s took an example, where i used props.time as a milisecond delay in SlowComponent via sleep() (Don’t mind my style lol ! just for quick)

When i set the default time delay as 1000ms. Changing count state now affecting by the delay. The rendering is delayed withtime.

Why is that?

In my previous post, i described the rendering process. If you haven’t read, in brief, changing state inside a component, which marks that component as needing updated, queues a render pass and start at that component and loop downwards to its children to calculate the changes.

Image for post
Image for post

When we change count, it happens at the level of App component, then it triggers a rendering for child components (CounterComponent, SlowComponent). Because sleep() function delays the work of render phase, slow down the calculation of React → resulting in the commit phase is slow down too, it slows to commit the changes to the real UI tree, even CounterComponent has already done diffing its tree, it still has to wait for SlowComponent, because they commit at once.

Colocate State to the rescue

You can use React.useMemo, to skip the rendering of SlowComponent, so that when increase count, it doesn’t has to wait for SlowComponent

But we don’t want our code become ugly monsters every where. More elegant approach is to move count state inside CounterComponent, because SlowComponent don’t use it. And when changing count inside CounterComponent, React won’t care about SlowComponent, ignore diffing it.

Image for post
Image for post

and the result is we can count extremely fast noww.

Image for post
Image for post

In real work, there always happen cases like this. Putting your state wisely will help you avoid redundant heavy calculations. That’s why this is important !

An App Screen usually come with different UI sections/container. We don’t want rendering a small container triggers the whole screen to rendering, mind your state’s purpose, and place it properly to gain better performance.

Summary

In this post, to summary, we learned how to make good use of state:

Have a good day guys !

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app