Classes
Before going into how hooks differ, let's look how classes process state updates.
When you call the base class setState
method, it enqueues a state change.
this.updater.enqueueSetState(this, partialState, callback, 'setState');
The updater
object it uses is defined here. Let's look at the implementation of enqueueSetState
.
First it gets a fiber
using the getInstance
function, which is an alias of a broadly named get
function.
const fiber = getInstance(inst);
It's a bit obscurely named, but all it does is take an object instance (your class component), and return its _reactInternals
property value.
It creates an update
object where payload
is the argument that was passed to setState
. It then schedules this update object, without executing it, and marks the component needs to be re-rendered using scheduleUpdateOnFiber
.
const update = createUpdate(eventTime, lane);
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;
}
const root = enqueueUpdate(fiber, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitions(root, fiber, lane);
}
The queue of previously scheduled updates is processed as the component is updated (the render triggered by setState
starting with our element as the root).
It calls updateClassInstance
that both executes the class's lifecycle methods, and returns whether the component needs to be re-rendered.
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
https://github.com/facebook/react/blob/a8c9cb18b7e5d9eb3817272a1260f9f6b79815a2/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L1142
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
If the state has changed, it will proceed to render the component.
Hooks
Contrary to a class component, which manages its own state internally, a function component relies on calling hooks to have React store the state "for you".
Every call to useState
(or any other hook that internally depends on storing a value) will add this piece of state to a per component linked list.
React relies solely on the order of this list to retrieve the right values for each hook on subsequent render passes. That's why you can only use hooks at the top level, and never in loops / conditionals. Otherwise React has no way of knowing which data belongs to which hook call.
React hooks each have 2 implementations , which are switched internally. One for when the component mounts, and one for updates. This makes sense as the logic for both cases is quite different.
On the first render, it uses mountState
.
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
// ...
mountWorkInProgressHook
is where the hook call's entry in the linked list is created.
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
Next mountState
initializes the state according to the argument of useState
.
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
It also creates an update queue, similar to the internal update queue of class components.
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
The setState
function you get back will put the arguments in the queue whenever you call it.
On update renders, useState
uses updateState
, which internally calls updateReducer
. It processes all updates in the queue.
Finally, to trigger a render when you call setState
, after adding the update to the queue, it schedules the element as a root that needs to be re-rendered. Source
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
Comparison
For the most part classes and functional components end up doing the same thing here.
- Add updates to a queue
- Mark the element as needing render
- Apply queued state updates as the component is rendered, before logic that uses the state
However they use a separate implementation for each of these things, leading to subtle differences.
When exactly is state updated?
With class components, you state is always going to get updated before it even calls the class's render
method.
For function components with hooks, the state updates happen as each hook is called.
While it's clearly a different approach, this difference doesn't have much practical implications. State updates happen in time in both cases.