Introduction
I have been using the useState
hook 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 useState
hook.
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.
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 thegetter
approach.- In order to make it work like
useState
,state
must be declared as a variable while maintaining thestate
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.”
As explained earlier, the state
values 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.
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
.
- The
useState
function in React returns an array with two elements: the currentstate
value and a function to update thestate
. The function to update thestate
is actually a special function called a "setter
" that you can call to update the state. - When
useState
is called, React gets the currentdispatcher
object fromReactCurrentDispatcher.current
. Thedispatcher
is a special object that keeps track of the state updates for the component. - When
useState
is called, React adds a new hook object to the array of hooks stored on thedispatcher
object. This hook object contains the current state value and a queue of pendingstate
updates. - 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. - When you call the
setter
function to update thestate
, React adds a newstate
update to the queue of pending updates for the current hook. The queue is then processed by thedispatcher
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