thumbnamil

UseState Deep Dive

Jul 8, 2023

  • React

How does useState work

Introduction

I have been using the useStatehook in React, but I am interested in understanding how it works internally. To gain a deeper understanding, I would like to dive into the implementation of the useStatehook.

useState in React

In React, when the state changes, the component is notified and it is re-rendered to reflect the updated state.

In class components, the internal state of the component was defined using state and the setState method was used to implement this logic. The render() method was used to detect state changes and update only the necessary parts.

However, functional components call the function again whenever a render is required.

To manage state in functional components, it is necessary to have information about the previous state when the function is called. React uses closures in this process.

Then, What is Closure?

To understand of useState you have to know about Clousre the concept in Javascript.

What is Closure

According to MDN, it explains Closure like this.

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function

In JavaScript, closures are created every time a function is created, at function creation time.

“In other words, it is a feature that remembers the lexical scope to which a function belongs, enabling it to access that scope even when executed outside of the lexical scope.”

function getName() { let name = 'GeoJung'; return function () { name = 'GeoJung Im'; return name; }; } const fullName = getName(); console.log(fullName());

An inner function can access variables in the lexical scope of its parent function, even after the parent function has completed and returned.

This means that even though the context information of the parent function, such as declared variables and functions, has already been cleared from the execution context queue, if a child function remains, it can still refer to the context information of the completed parent function.

I found this explanation to be the most understandable: as TKDODO puts it, the principle of closure is like taking a snapshot of the function every time it is created.

image

Every time a new function is created, the old picture is discarded and a new picture is taken.

The useState method uses this closure to remember the state of the function.

Let’s implement useState using the concept of closure

function useState(initialState) { let value = initialState; const state = () => value; const setState = (newValue) => { value = newValue; }; return [state, setState]; } const [state, setState] = useState(0); const increment = () => { setState(state() + 1); console.log(state(), 'state'); }; const decrement = () => { setState(state() - 1); console.log(state(), 'state'); }; return ( <div> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> </div> );

This is an implementation of the useState function using the concept of closure. Although state and setState are actually used after the useState call, the closure retains the innerState value, so they can still be accessed afterwards. However, there are several issues to address in order to make it work like useState

Problems

  • state is implemented as a function using the getter approach.
  • In order to make it work like useStatestate must be declared as a variable while maintaining the state value.

To maintain the state value while declaring state as a variable, React resolves this issue by declaring state outside of useState so that it can preserve the state value.

This code is based on Yardley’s article, which provides detailed explanations on why order matters in Hooks and how they work internally. I highly recommend reading it.

let state = []; let setters = []; let firstRun = true; let cursor = 0; function createSetter(cursor) { return function setterWithCursor(newVal) { state[cursor] = newVal; }; } export function useState(initVal) { if (firstRun) { state.push(initVal); setters.push(createSetter(cursor)); firstRun = false; } const setter = setters[cursor]; const value = state[cursor]; cursor++; return [value, setter]; }

The state declared outside of useState is converted to an array format. By managing state as an array, we can solve the problem of having multiple components using useState.

The state declared with useState is stored in the array in order. The state array can be accessed through a key that uniquely identifies the component, and the state values are stored in the array in the order of the component.

Rules of Hooks

According to the React official documentation“It’s important to note that Hooks should always be called in the same order every time a component renders.”

image

As explained earlier, the statevalues of a component are stored in an array that is keyed by the component, so if Hooks are used inside a conditional statement or a regular JavaScript function, the order of the saved state values may not match the order in which they were originally saved, resulting in the incorrect state being referenced. Therefore, if Hooks are used inside a loop or conditional statement, an error will occur.

0*n69pSYKGdrkHJEGG.png

React Hook should be called in the exact same order on every render of the component.

How useState works?

To understand how useState works, I looked at the React code.

export function useState(initialState) { const dispatcher = resolveDispatcher() return dispatcher.useState(initialState) }

The useState function takes initialState as an argument, and it returns the result of passing initialState to the useState method of the dispatcher object, which is obtained from calling resolveDispatcher.

function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; invariant( dispatcher !== null, ); return dispatcher; }

When we dive into the resolveDispatcher function, we can see that it returns the current value of ReactCurrentDispatcher, which is a global variable that points to the current dispatcher being used by React.

const ReactCurrentDispatcher = { current: null, } export default ReactCurrentDispatcher

This current variable is used to determine which dispatcher to use for useState.

  1. The useState function in React returns an array with two elements: the current state value and a function to update the state. The function to update the state is actually a special function called a "setter" that you can call to update the state.
  2. When useState is called, React gets the current dispatcher object from ReactCurrentDispatcher.current. The dispatcher is a special object that keeps track of the state updates for the component.
  3. When useState is called, React adds a new hook object to the array of hooks stored on the dispatcher object. This hook object contains the current state value and a queue of pending state updates.
  4. Each hook object in the array contains the current state value and a queue of pending state updates. The useState function returns the current state value and the setter function that can be used to update the state.
  5. When you call the setter function to update the state, React adds a new state update to the queue of pending updates for the current hook. The queue is then processed by the dispatcher on the next render, causing the component to re-render with the updated state values.

Summary

  • In React, to manage the state of a functional component, we use values stored outside the component and access them through closure to compare and modify the state.
  • The useState hook modifies the value outside the component, so immediately after a state change, the component still references the previous value.
  • Additionally, each component's state information is stored in an array, so if a state-changing hook is used within a loop or conditional statement, it may reference the wrong value due to the hook being called in the wrong order.

References

React hooks: not magic, just arrays

react/ReactHooks.js at v16.12.0 · facebook/react

Hooks, Dependencies and Stale Closures

useState — React

Deep dive: How do React hooks really work?

[React] 클로저와 useState Hooks

React 톺아보기 — 03. Hooks_1 | Deep Dive Magic Code