How hooks work
Introduction
Hooks were introduced in React v16.8.0, and since then they changed the way React apps were written. Before them, we used to create class component for anything involving state or lifecycle logic. Hooks made function component the new defacto of writing react apps.
They were a huge addition that simplified a lot of things. I am not stating here
that they are better than class components, but they offer a simpler way
to reason about components and to deal with them, and especially they allow
escaping dealing with this
.
This is not an explanation of React hooks themselves as they are pretty well documented in the official documentation. We will see how they are written into React.
Context
In the previous sections, precisely in how function components get rendered;
we've seen that we set a Dispatcher
based on whether your component is
mounting for the first time or updating. So first, let's demystify this
dispatcher.
ReactCurrentDispatcher
in the renderWithHooks
function, we set the ReactCurrentDispatcher.current
. Which is a plain
javascript object with all the hooks implementations that React has.
The goal of having the dispatcher object is to limit the behavior of your hooks' usage:
- You cannot use hooks outside the render phase: This means that hooks won't work if you just decide to call the function component manually when React isn't rendering. You are probably familiar with the thrown error.
- Hooks behavior on mount and update isn't the same: as we will see later in
this section, on mount, the main goal of the dispatcher is to
reserve
as place for your hook call and initialize it, but on update it will perform the update logic which is different.
Like we said, the dispatcher contains as many properties as React hooks:
export const AllDispatchers: Dispatcher = {
readContext,
use,
useCallback: hook,
useContext: hook,
useEffect: hook,
useImperativeHandle: hook,
useInsertionEffect: hook,
useLayoutEffect: hook,
useMemo: hook,
useReducer: hook,
useRef: hook,
useState: hook,
useDebugValue: hook,
useDeferredValue: hook,
useTransition: hook,
useSyncExternalStore: hook,
useId: hook,
};
There are several dispatchers, but we will only discuss the main four:
ContextOnlyDispatcher
: This dispatcher will prevent you from using hooks outside the render phase. It will throw the famousInvalid hook call
error.HooksDispatcherOnMount
: This dispatcher contains hooks implementation for components when mounting for the first time.HooksDispatcherOnUpdate
: This dispatcher contains hooks implementation for components when they are updating.HooksDispatcherOnRerender
: This dispatcher contains hooks implementation when components are re-rendering: when they perform a render phase update or when they rendered twice in dev mode.
How hooks are modelled
Each function component may have calls for any of the supported hooks. Remember,
all hooks calls occur withing the renderWithHooks
function (exception for
the hooks for re-renders, they are called from within the renderWithHooksAgain
function).
Hooks are store into the memoizedState
property of the related Fiber
.
A hook is stored inside React as a plain object with the following properties:
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
Where:
memoizedState
: contains the hook "state" (or value)baseState
: used by state hooks to store the initial valuebaseQueue
:queue
: the UpdateQueue object used by state hooks to store many thingsnext
: the next hook.
As you may have guessed, the next
property references in the next hook your
component uses. Thus, hooks are modelled as a linked list of the previous
data structure.
Each hook have its own specification when it comes to what it stores inside these properties, some hooks don't use all of these properties, obviously.
Notice how this data structure doesn't contain any information about the hook being used, hooks relies on call order and should ALWAYS be preserved.
Dan Abramov wrote an outstanding blog post explaining this design choice.
Hooks examples
Assuming we are rendering the following component:
function MyFunctionComponent(props) {
const [count, setCount] = React.useState(0);
// please don't do this, this is only for demo purposes
const isMounted = React.useRef(false);
// please don't do this, this is only for demo purposes
const mountDate = React.useMemo(() => Date.now(), []);
React.useEffect(() => {
function handler() {
console.log('window is focused')
}
window.addEventListener("focus", handler);
return () => window.removeEventListener("focus", handler);
}, []);
return <span>Count is {count}</span>
}
Rendering this component will result in having a Fiber
of tag
FunctionComponent
with the following hooks linked list:
let memoizedState = {
// useState
"memoizedState": 0,
"baseState": 0,
"baseQueue": null,
"queue": {
"pending": null,
"lanes": 0,
"lastRenderedState": 0
},
"next": {
// useRef
"memoizedState": {
"current": false
},
"baseState": null,
"baseQueue": null,
"queue": null,
"next": {
// useMemo
"memoizedState": [
1700218172414,
[]
],
"baseState": null,
"baseQueue": null,
"queue": null,
"next": {
// useEffect
"memoizedState": {
"tag": 9,
"inst": {},
"deps": [],
"next": "the same effect .. removed for clarity"
},
"baseState": null,
"baseQueue": null,
"queue": null,
"next": null
}
}
}
}
How hooks on mount work
The purpose on hooks on mount as stated before is to grab a place in the linked list of the hooks.
So, all hooks implementations on mount will first perform the following:
const hook = mountWorkInProgressHook();
The mountWorkInProgressHook
function will create the previous data structure
and then set is as the memoizedState
property of the currentlyRenderingFiber
.
mountWorkInProgressHook
Implementation
The mount in progress hook function is implemented as follows:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
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;
}
return workInProgressHook;
}
- First, it will create the hook object
- Then, if it is the first hook of the list, it will attach it to the
memoizedState
of thecurrentlyRenderingFiber
and set this hook at theworkInProgressHook
- Or else, it will attach it to the
next
property of theworkInProgressHook
.
And that's it!
Depending on the hook, other things will be performed, we will see them separately for each supported hook.
How hooks on update work
When your component is updating (not its first ever render), each supported hook call will start with the following expression then will follow up with the specific work.
const hook = updateWorkInProgressHook();
updateWorkInProgressHook
is a bit more complex than the mount one, but its purpose it to detect the next
workInProgressHook
too. it used for both updates and re-renders, so it assumes
that there is either a current
hook object that can be cloned or a
work-in-progress
from a previous render that can be reused.
The first part of this function then is to find the currently rendered hook
value. It will check the current
rendered fiber's memoizedState
property if the currentHook
module variable is null, or else it takes its
next
property:
// at module level:
let currentHook: null | Hook = null;
// inside updateWorkInProgressHook
let nextCurrentHook: null | Hook;
if (currentHook === null) {
// the current rendered fiber
const current = currentlyRenderingFiber.alternate;
// already mounted
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
// first mount
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
Now, after detecting the current rendered (painted) hook value, React will then try to find its alternate (the being rendered one):
// at module level:
let workInProgressHook: null | Hook = null;
// inside updateWorkInProgressHook
let nextWorkInProgressHook: null | Hook;
// first hook of the list, take it from the being rendered fiber
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// or else, it is the next hook
nextWorkInProgressHook = workInProgressHook.next;
}
It is important to note that when we start updating a component, the memoized state property is reset and set to null.
Now, we have the currently painted hook value, and the being rendered one.
When there is a nextWorkInProgressHook
, this means that we already have
started rendering then we are rendering again without committing and finishing
the render, and thus, we will reuse it as is:
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
}
Or else, if the nextCurrentHook
is null, that we are rendering more hooks
than the previous render, which is against hooks rules, and then React will
throw an error.
When nextCurrentHook
isn't null, this means that we should clone the previous
hook and use it as a base:
// React code
if (nextWorkInProgressHook !== null) {
// previous code
} else {
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// This is the initial render. This branch is reached when the component
// suspends, resumes, then renders an additional hook.
// Should never be reached because we should switch to the mount dispatcher first.
throw new Error(
'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
);
} else {
// This is an update. We should always have a current hook.
throw new Error('Rendered more hooks than during the previous render.');
}
}
currentHook = nextCurrentHook;
// clone from the currently painted hook
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
How hooks on rerender work
Re-rendering components is an internal term in the React codebase that means that the component either scheduled a render phase update or that we are replaying it in development mode.
If you take a close look at the HooksDispatcherOnRerender
dispatcher, you will
notice that it is the same as the HooksDispatcherOnUpdate
except for:
useReducer: rerenderReducer
, useState: rerenderState
,
useDeferredValue: rerenderDeferredValue
and useTransition: rerenderTransition
.
This dispatcher is set from the renderWithHooksAgain
function. I will just copy Andrew's comment about this function:
// This is used to perform another render pass. It's used when setState is
// called during render, and for double invoking components in Strict Mode
// during development.
//
// The state from the previous pass is reused whenever possible. So, state
// updates that were already processed are not processed again, and memoized
// functions (`useMemo`) are not invoked again.
//
// Keep rendering in a loop for as long as render phase updates continue to
// be scheduled. Use a counter to prevent infinite loops.
How each hook works
We will follow the presence of hooks in the dispatchers order to explain them.
How use works
The use
hook is a new hook that will replace the throw promise
pattern
introduced for suspending components that still wait for data.
Suspending using throw promise
was there since too long, but was never
official, and this hook is introduced as a viable alternative.
Signature
The use
hook is defined here.
function use<T>(usable: Usable<T>): T {
// [Not Native Code]
}
It accepts either a promise or context type.
The use hook doesn't rely on mountWorkInProgressHook
and updateWIPHook
, so
it can be called conditionally and doesn't obey the rules of hooks.
Implementation
As stated before, use
accepts thenabled
and Context
:
Context
When the provided object to use
is a React Context
, it will just delegate
the work to the readContext
function. It will be discussed and explained
in the useContext
section.
if (
usable.$$typeof === REACT_CONTEXT_TYPE ||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
const context: ReactContext<T> = usable;
return readContext(context);
}
So, use
will allow you to conditionally subscribe to a context while escaping
the rules of hooks 🤯
Thenable
When a thenable object is provided, React will call the internal useThenable
function:
function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
if (typeof usable.then === 'function') {
const thenable: Thenable<T> = usable;
return useThenable(thenable);
}
// ... other code
}
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}
We've reached a point where the useThenable
function is clearly behind
the use
hook's work:
Besides of initializing and incrementing the thenable state (which I will not
explain now), useThenable calls trackUsedThenable
which will do the whole work.
function useThenable<T>(thenable: Thenable<T>): T {
// Track the position of the thenable within this fiber.
const index = thenableIndexCounter;
thenableIndexCounter += 1;
if (thenableState === null) {
// createThenableState returns a plain javascript array
thenableState = createThenableState();
}
const result = trackUsedThenable(thenableState, thenable, index);
// ... other code
return result;
}
Let's then dive into trackUsedThenable
:
Part 1: Add the thenable to the array of thenables:
Please pay attention to Sophie's comment: if the there was a thenable at the same position, we will reuse the former one because they should technically point to the same value. Don't ask me of this design choice.
const previous = thenableState[index];
if (previous === undefined) {
thenableState.push(thenable);
} else {
if (previous !== thenable) {
// Reuse the previous thenable, and drop the new one. We can assume
// they represent the same value, because components are idempotent.
// Avoid an unhandled rejection errors for the Promises that we'll
// intentionally ignore.
thenable.then(noop, noop);
thenable = previous;
}
}Part 2: Track the thenable There are two cases, whether we previously tracked this thenable, or it is the first time we encounter it.
Tracking the thenable is adding a
then(onFullfilement, onRejection)
callbacks that will mutate the thenable itself:Read well the following code and grasp it:
const pendingThenable: PendingThenable<T> = thenable;
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<T> = thenable;
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<T> = thenable;
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);But before tracking like this, if the thenable was already tracked, we only verify its status:
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError = thenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
// ... other code
}- When status is
fulfilled
, theuse
hook will return the value - When status is
rejected
, theuse
hook will throw the error
When the status is
pending
, React will throw a special exception object calledSuspenseException
to suspend the tree until the thenable resolves or rejects.This will lead to the component rendering only if it has data, and throwing in the other cases.
noteThe use hook will require you to put an Error boundary in the tree to intercept rejections.
- When status is
The use
hook will require you to manually cache/memoize the promises.
The React.cache
experimental API is designed to help you with that.
Examples
Let's say we want to get the user details from the public jsonplaceholder API.
To achieve that, we will create a small cache to help us memoizing the promises and thus avoid infinite renders. So, let's create a dumb memoizer for functions:
// we assume that we will cache with one parameter
// which will be the user id.
// React.cache is a general solution for this.
// for clarity, we'll use only userId
function createCache(asyncFunc) {
let cache = {};
return function exec(...args) {
let cacheId = args[0];
let existing = cache[cacheId];
if (existing) {
return existing;
}
let result = asyncFunc.apply(null, args);
cache[cacheId] = result;
return result;
};
}
Let's then create a dumb error boundary for that too:
class ErrorBoundary extends React.Component {
state = { error: null };
componentDidCatch(error) {
this.setState((prev) => ({ ...prev, error }));
}
render() {
const { error } = this.state;
if (error) {
return (
<>
<pre>{error.toString()}</pre>
<button
onClick={() => this.setState((prev) => ({ ...prev, error: null }))}
>
Reset
</button>
</>
);
}
return this.props.children;
}
}
And finally, let's exploit this code:
async function fetchUserById(userId) {
let result = await axios.get(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
return result.data;
}
let getUserDetails = createCache(fetchUserById);
let IDS = [1, 2, 3, 4, 5, 10, 11];
function UserDetails({ id }) {
let details = React.use(getUserDetails(id));
return (
<details open>
<pre>{JSON.stringify(details, null, 4)}</pre>
</details>
);
}
function Example() {
let [userId, setUserId] = React.useState(IDS[0]);
return (
<div className="App">
{IDS.map((id) => (
<button
onClick={() => {
setUserId(id);
}}
key={id}
>
{`User ${id}`}
</button>
))}
<React.Suspense fallback={`Loading user ${userId}`}>
<UserDetails id={userId} />
</React.Suspense>
</div>
);
}
export default function App() {
return (
<ErrorBoundary>
<Example />
</ErrorBoundary>
);
}
You can view and manipulate this demo here:
How useCallback works
The useCallback
hook allows you to keep a function reference until
a dependency changes.
Signature
useCallback
is defined as follows:
function useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// [Not Native Code]
}
The previous function doesn't exist directly, as stated before, there is
mountCallback
and updateCallback
functions, with the same signature:
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// [Not Native Code]
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// [Not Native Code]
}
Implementation
On mount
When your component renders for the first time while using useCallback
, the
call will be intercepted by mountCallback
, which is probably the easiest hook:
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// step 1
const hook = mountWorkInProgressHook();
// step 2
const nextDeps = deps === undefined ? null : deps;
// step 3
hook.memoizedState = [callback, nextDeps];
return callback;
}
- Step 1: Mount the hook data structure seen in the top of this section
- Step 2: Define the dependencies to use, if the parameter is omitted,
null
is used. - Step 3: Store the callback and dependencies in the
memoizedState
of the hook.
useCallback
will return whatever value you throw at it, usually we give either
an inline function defined directly there, or a function defined in the component
body.
So, in mount, useCallback don't care about your dependencies, it will only store them for later usage.
On Update
On updates, the goal is to give you a new function reference only if one dependency changes.
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// step 1
const hook = updateWorkInProgressHook();
// step 2
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
// step 3
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// step 4
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
// step 5
hook.memoizedState = [callback, nextDeps];
return callback;
}
- Step 1: Create or reuse the hook data structure object.
- Step 2: Infer the dependencies array, or
null
if nothing is provided. - Step 3: When dependencies aren't null, which means that we are memoizing things (undefined as second argument means that we won't memoize anything), we will proceed to compare them with the previous deps.
- Step 4: This compares the previous and next dependencies, if they are
the same, the previous value (the first element in the
memoizedState
array) is returned. We'll see the comparison in a few. - Step 5: When dependencies changed or that we aren't using them, similarly
to
mountCallback
, we store[callback, nextDeps]
into thememoizedState
property of the hook object.
The areHookInputsEqual
function is used in all hooks that use the dependencies array. It will:
- Always
return
false when there are no previous dependencies, which instructs React to recompute the hook returned value. In human words, this means that our hook doesn't use any dependencies and will be refreshed every render. - Loop over both arrays and use
Object.is
to compare individual values.
How useContext works
The useContext
hook allows you
to read and subscribe to a React Context
value.
Signature
useContext
hook
is defined as follows:
function readContext<T>(context: ReactContext<T>): T {
// [Not Native Code]
}
Where the unique parameter refers to a React context object created by the
React.createContext
API.
Implementation
useContext
uses the readContextForConsumer
function:
export function readContext<T>(context: ReactContext<T>): T {
// ...dev checks
return readContextForConsumer(currentlyRenderingFiber, context);
}
The readContextForConsumer
is responsible for giving you the current context
value and also subscribe to it for future changes. Let's dig into its
implementation:
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
// step 1
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
// step 2
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
// step 3
} else {
}
}
- Step 1: Decides on the internal context value based on the
isPrimaryRenderer
renderer property. I don't have much experience with this property, but you set it while creating a custom React renderer. Primary means that your renderer renders the whole page, and secondary means that you are writing a renderer meant to be used on top of another one. In our case, which is React-DOM, it is a primary renderer, so we take the_currentValue
value. - Step 2: The second step will actually bail out reading and subscribing to
this context. React uses the
lastFullyObservedContext
module variable, which seems to be unused. I mean, I haven't seen it being written in the whole codebase. - Step 3: The third step is where subscription occurs, let's dig into it.
How Context subscription works
The context subscription is stored in the fiber.dependencies
property in
a linked list fashion (again):
// simplified
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
const value = context._currentValue;
const contextItem = {
context: (context as ReactContext<any>),
memoizedValue: value,
next: null,
};
}
Then, when this is the first useContext
in this component, it will add the
following object as dependencies
:
// simplified
// the lastContextDependency variable is reset in the prepareToReadContext
// function called when we are about to update components
// (updateFunctionComponent and others)
if (lastContextDependency === null) {
lastContextDependency = contextItem;
// consumer is the work in progress fiber
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem
}
}
Or else, it will add the context item as the next
property of the previous one:
if (lastContextDependency === null) {
// ...
} else {
lastContextDependency = lastContextDependency.next = contextItem;
}
And that's it!
Updates
When a component rendering a ContextProvider
gets updated, React will check
on the value
prop
, and if it changed, it will propagate the changes.
This section will be explained in how rendering of a ContextProvider
works.
Similarly to the use
hook, useContext
can be called conditionally on render.
But you cannot call it inside other hooks or outside the render phase. Because it needs the currently rendering fiber to be able to perform subscription.
How useEffect works
The useEffect
hook allows you
to register passive effects on your component.
Passive effects run as the last part of the commit
phase of the render.
It is either synchronous for SyncLane
s for Asynchronous
for the rest lanes.
From the docs:
useEffect is a React Hook that lets you synchronize a component with an external system.
This means that you should only use this hook to keep your component in sync with external systems, such as the browser APIs (focus, resize, blur...) or some external stores.
Signature
The useEffect
hooks is defined as follows:
function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// [Not Native Code]
}
It accepts two parameters:
create
: the effect creation function, it doesn't accept any parameters, and return either nothing, or acleanup
function. The cleanup function is responsible for cleaning the effect work: such as unsubscribing from an event.deps
: The optional dependencies array, that will result in running the effect creation again whenever a dependency changes. If this parameter is omitted, the effect will run at the end of every render phase.
If your component performs a render phase state update, the effect won't run twice. But rather, the effects are ran at the commit phase, after the render is performed.
Implementation on mount
Like normal hooks, this hook relies on mountWorkInProgressHook()
, which
creates the object seen in the start of this section.
mountEffect
calls a function called mountEffectImpl
.
mountEffectImpl
is called from all the other effect hooks (useLayoutEffect
,
useInsertionEffect
and other hooks that add special effects.)
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}
Let's discuss the previous snippet before going any longer, to do so, we need
to observe the mountEffectImpl
signature:
function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// [Not Native Code]
}
fiberFlags
: Flags that will be added to the component using the effecthookFlags
: Flags that define the effect itself, possible values are:Insertion
,Layout
andPassive
.Passive
is used foruseEffect
.create
: The effect functiondeps
: The effect dependencies
Finally, let's take a look at the useEffectImpl
function before diving more
in this maze:
function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// step 1
const hook = mountWorkInProgressHook();
// step 2
const nextDeps = deps === undefined ? null : deps;
// step 3
currentlyRenderingFiber.flags |= fiberFlags;
// step 4
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
createEffectInstance(),
nextDeps,
);
}
- Step 1: Mount the hook data structure seen in the top of this section
- Step 2: Define the dependencies to use, if the parameter is omitted,
null
is used. - Step 3: Add the received fiberFlags to the currently rendering fiber. In
the case of
useEffect
, it is:PassiveEffect | PassiveStaticEffect
, which is a natural number (8390656
at the moment of writing these words). - Step 4: Store the
memoizedState
value of this hook, which is the result of calling thepushEffect
.
The createEffectInstance
function
just returns the object { destroy: undefined }
. It will be used to store the
effect cleanup function (if any).
So, the last part of this is to take a look at the pushEffect
function:
function pushEffect(
tag: HookFlags, // useEffect: Passive
create: () => (() => void) | void,
inst: EffectInstance, // { destroy: undefined }
deps: Array<mixed> | null,
): Effect {
// [Not Native Code]
}
Create the effect object
This object is created every render for every effect you use, it stores the relevant information needed to perform well.
const effect: Effect = {
tag, // The hook flag
create, // the provided effect function
inst, // { destroy: undefined }
deps, // the provided dependencies or null
// Circular
next: null, // this will be set next
};
Link the effect to the function component update queue
Next, React will reference the currentlyRenderingFiber.updateQueue
property,
and if null, it will initialize it:
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// this creates a circular reference (it will be teared a part when committing)
componentUpdateQueue.lastEffect = effect.next = effect;
}
The update queue created by createFunctionComponentUpdateQueue
looks like this:
const updateQueue = {
lastEffect: null,
events: null,
stores: null,
}
// when the memoCache feature is enabled, it will add a memoCache property
// initialized by null
It is used as a circular linkedList, when we store the lastEffect (its next
property will then point to the first
effect in the list.)
When the component updateQueue
is already initialized (we've called an
effect before in this render, or another hook initialized it), React will take
the lastEffect
property and:
- If
null
(may happen if the updateQueue was initialized by events or stores and not by an effect), it will do the same as before: will create a circular reference by theeffect
object and itself, and store it in thelastEffect
property of the queue.const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// take a look next
} - Not
null
means that we called an effect hook before in this render pass, and in this case, React will execute this code:Don't be confused, let's break the previous code:const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;- First, reference the first effect on the list (well, since it is a circular
linked list, the first is the
next
of the last). - Add the new effect as the
next
of the previouslastEffect
: well, this is the new last. - The new effect (which is the new last) will point then to the
firstEffect
in itsnext
property. - Finally, mark the new effect as the lastEffect in the list in the component
updateQueue
.
- First, reference the first effect on the list (well, since it is a circular
linked list, the first is the
Finally, the pushEffect
function will return the new effect object defined
below and store it in the hook.memoizedState
.
Implementation on update
On updates, useEffect
will call updateEffect
from theHooksDispatcherOnUpdate
dispatcher, which will delegate to the
updateEffectImpl
function.
These functions have the same signature as the mount ones.
The only difference to note is the passed fiberFlags
: On mount we passed
PassiveEffect | PassiveStaticEffect
, and on update we pass only PassiveEffect
.
At the moment of writing these words, I cannot seem to know what causes this
difference, I cannot find any place in the React codebase
where thePassiveStaticEffect
is used to build any decision. Apart from some
todos, so maybe an incoming/unfinished feature.
But there is also the following comment where static flags are declared:
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
export const RefStatic = /* */ 0b0000001000000000000000000000;
export const LayoutStatic = /* */ 0b0000010000000000000000000000;
export const PassiveStatic = /* */ 0b0000100000000000000000000000;
export const MaySuspendCommit = /* */ 0b0001000000000000000000000000;
In addition to passed parameters, the implementation surely differ, because on updates, we need to check if dependencies changed:
So, first, the updatewWorkInProgressHook
function is called.
function updateEffectImpl(
fiberFlags: Flags, // PassiveEffect
hookFlags: HookFlags, // HookPassive for useEffect
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// the effect from the previous render
const effect: Effect = hook.memoizedState;
// the effect instance from the previous render, it will be reused
const inst = effect.inst;
// currentHook is null on initial mount when rerendering after a render phase
// state update or for strict mode.
// we've seen currentHook at the start of this section, in updateWIPHook
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
// pushEffect was described on mount above, we call it here too
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
return;
}
}
}
// add the fiberFlags to the fiber.flags
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
inst,
nextDeps,
);
}
This is how useEffect works, the effect functions are executed in the commit phase. During render, we just store the relevant information.
Please refer to the how commit works
section
for more information about the timing of invocation of each type of effects.
How useImperativeHandle works
The useImperativeHandle
hook
is defined in the official docs as:
useImperativeHandle is a React Hook that lets you customize the handle exposed as a ref.
In human words for the rest of us, this means that it lets you override what
a component is exposing as ref (the handle), for example, if you want to add a
function to your custom button, let's say, you want to add a sayHiTo(name)
function to it that will show an alert
, and so on.
Signature
The useImerativeHandle
is defined as follows:
function mountImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
// [Not Native Code]
}
- ref: The ref, created by
useRef
orcreateRef
, can be also a ref callback - create: The function that will return the new ref '
handle
' - deps: The hook dependencies, the create function will be called again when a dependency changes.
Implementation on mount
When the component using useImperativeHandle
renders for the first time,
it will call the mountImperativeHandle
function
which is implemented as follows:
// step 1
const effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null;
// step 2
mountEffectImpl(
UpdateEffect | LayoutStaticEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
- Step 1: Compute the real hook dependencies: The provided ones in addition to the ref object itself. Maybe we can skip adding the ref to the array and suppose the developer should add it manually ? But this won't be backward compatible anyway.
- Step 2: The second step is to mount an effect (wait! what ? 😳)
Yes, you've read that right, useImperativeHandle
will insert a special layout
effect whose create
function is the imperativeHandleEffect
function
During the commit phase, React will attach refs at the Layout
phase. That's
why the whole work is used as a layout effect.
Implementation on update
On updates, useImperativeHandle
will calculate the deps like on mount,
and then call to updateEffectImpl
with only UpdateEffect
as fiber flags.
This brings us to the real work.
How imperativeHandleEffect
works
Signature
function imperativeHandleEffect<T>(
create: () => T,
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
): void | (() => void) {
// [Not Native Code]
}
Implementation
The implementation will perform the work based on whether the passed ref is
a ref object or a ref callback, and either ways will call your passed create
function and return a cleanup function for the layout effect:
if (typeof ref === 'function') {
// step 1
const refCallback = ref;
// step 2
const inst = create();
// step 3
refCallback(inst);
// step 4
return () => {
refCallback(null);
};
}
- Step 1: keep track of the passed ref callback reference
- Step 2: call the useImperativeHandle create function, that will produce the new ref handle
- Step 3: call the ref callback with the resulting handle
- Step 4: return the layout effect cleanup to call the ref callback again with a null value
Alternatively, when the passed ref
is a refObject
, then
imperativeHandleEffect
will:
// this is origannly an else if
if (ref !== null && ref !== undefined) {
// step 1
const refObject = ref;
// step 2
const inst = create();
// step 3
refObject.current = inst;
// step 4
return () => {
refObject.current = null;
};
}
- Step 1: keep track of the passed ref object reference
- Step 2: call the useImperativeHandle create function, that will produce the new ref handle
- Step 3: attach the resulting ref handle to the
current
property of the ref object - Step 4: return the layout effect cleanup that will reset the
current
property to null
And that's it!
As stated before, the imperativeHandleEffect
will be invoked during the
layout effect iteration of the commit phase. It won't be called right away
during render.
How useInsertionEffect works
The useInsertionEffect
hook
by definition:
Allows inserting elements into the DOM before any layout effects fire
As stated in its official documentation section, it should only be used by
css-in-js library authors, if not, please use useLayoutEffect
or useEffect
.
Signature
Same as other effects, the useInsertionEffect
is defined as follows:
function useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// [Not Native Code]
}
Implementation
The useInsertionEffect
implementation is the same as useEffect
,
the only difference is the flags
being passed on mount to mountEffectImpl
and on update to updateEffectImpl
:
- On mount: React passes
UpdateEffect
as fiber flags andHookInsertion
as hook flags. - On update: React passes
UpdateEffect
as fiber flags andHookInsertion
as hook flags.
And that's it! All effects only differ in the flags.
How useLayoutEffect works
The useLayoutEffect
hook
by definition is:
Is a version of useEffect that fires before the browser repaints the screen.
Well, that's not totally true when it comes to comparing it to useEffect,
the how commit works
section
will reveal more information about that.
useLayoutEffect
allows you to register effect that run after the render phase
synchronously after mutating the dom elements.
Its synchronous nature blocks the browser's main thread and thus prevent it from
partially painting the new render resulting UI. Which makes us often say:
useLayoutEffect
runs before the browser's paint.
useLayoutEffect
runs at the same time as ClassComponent
lifecycle methods
(componentDidMount
and componentDidUpdate
)
Signature
Same as other effects, the useInsertionEffect
is defined as follows:
function useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// [Not Native Code]
}
Implementation
The useLayoutEffect
implementation is the same as useEffect
,
the only difference is the flags
being passed on mount to mountEffectImpl
and on update to updateEffectImpl
:
- On mount: React passes
UpdateEffect | LayoutStaticEffect
as fiber flags andHookLayout
as hook flags. - On update: React passes
UpdateEffect
as fiber flags andHookLayout
as hook flags.
And that's it! All effects only differ in the flags.
How useMemo works
The useMemo
hook allows you to
cache a value until a dependency changes.
Signature
useMemo
is defined as follows:
function useMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
// [Not Native Code]
}
Where:
nextCreate
: The function that will output our cached valuedeps
: The dependencies
Implementation on mount
On mount, useMemo
will call mountMemo
which is defined as follows:
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
// step 1
const hook = mountWorkInProgressHook();
// step 2
const nextDeps = deps === undefined ? null : deps;
// step 3
const nextValue = nextCreate();
// step 4
hook.memoizedState = [nextValue, nextDeps];
// step 5
return nextValue;
}
- Step 1: create the hook object on mount
- Step 2: calculate the deps to use, either the provided ones or
null
- Step 3: calculate the initial memo value
- Step 4: store
[nextvalue, nextDeps]
as thememoizedState
of the hook - Step 5: return the cached value
When in development mode and under StrictMode, React will call the nextCreate
twice:
// initialized in the renderWithHooks function
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
Implementation on update
On updates, useMemo
will call updateMemo
which is defined as follows:
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
// step 1
const hook = updateWorkInProgressHook();
// step 2
const nextDeps = deps === undefined ? null : deps;
// step 3
const prevState = hook.memoizedState;
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// step 4
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
// step 5
const nextValue = nextCreate();
// step 6
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
- Step 1: create the hook object on update (or reuse from incomplete render)
- Step 2: calculate the deps to use, either the provided ones or
null
- Step 3: reference the previous rendered value (even if the render was incomplete, use the rendered value, because it matters)
- Step 4: when dependencies are the same, the previously cached value is returned
- Step 5: compute again the cached value using the passed
nextCreate
memo function - Step 6: store the
[nextValue, nextDeps]
along with the deps in thememoizedState
of the hook and return the new cached value.
And that's it! useMemo
is similar to useCallback
in terms of implementation
and complexity, the difference is that useMemo
calls your function and
useCallback
will give it back to you.
How useReducer works
The useReducer
hook allows
you to add a reducer to your component.
A reducer
is a function that accepts two arguments: the current value
and
an action
to apply on that value, then return a new value
. Like this:
function reducer(prevValue, action) {
// perform some logic based on the action and the value
// ...
// then return a new value
}
Signature
The useReducer
hook is defined as follows:
function useReducer<S, I, A>(
reducer: (state: S, action: A) => S,
initialArg: I,
init?: (initialValue: I) => S,
): [S, Dispatch<A>] {
// [Not Native Code]
}
Where the arguments are:
- reducer: The reducer function
- initialArg: The initial value
- init: An optional initializer function that receives the
initialArg
And it returns an array containing two elements:
- state: the state value
- dispatch: a function that accepts the
action
to pass to our reducer.
Implementation on mount
When a component using useReducer
mounts for the first time, the
mountReducer
function
gets called. Let's break into it:
Mount the work in progress hook:
As you would expect, the first thing to do is this one. If you don't understand why, you probably did skip this whole section 😑
const hook = mountWorkInProgressHook();
Compute the initial state: The initial state computation depends on whether you provided the third parameter:
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = (initialArg as S);
}It is either the
initialArg
or the result of your function about it.Assign the hook
memoizedState
propertyThis unveils what this hook stores in the
memoizedState
property. But not it also populates thebaseState
property at this stage.hook.memoizedState = hook.baseState = initialState;
Create an UpdateQueue object and assign it
The update queue is an object referring to the internal state of
useReducer
, we won't deal with it here, but mostly when the component is updating or when React is scheduling/processing state updates. But it is important to take a look at its shape and referenced things.At first, it only references the initially given
reducer
and the computedinitialState
const queue: UpdateQueue<S, A> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState,
};
hook.queue = queue;Create the dispatch function and return
const dispatch: Dispatch<A> = (queue.dispatch = dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue,
));
return [hook.memoizedState, dispatch];The dispatch function is very important and is defined elsewhere,
mountReducer
will just give it two of the three parameters it accepts. Let's break it.
How dispatchReducerAction
works
dispatchReducerAction
is defined as follows:
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
// [Not Native Code]
}
Its job is to tell React to schedule an update on the desired fiber
and its
queue
(a component can have several useReducer
s, the queue holds all the
pending updates to be processed), using the given action
.
In other terms, this is the dispatch
function useReducer
and useState
give
you.
So, now, you are reading how the stateSetter works for function components.
The rest of this part would be easier if you've already read
the how root.render() works
section.
We won't explain again many things to keep this part easy. Scheduling updates works almost the same.
Request an update lane
const lane = requestUpdateLane(fiber);
The very first step is to infer the
lane
for this upcoming update, for example:- If your application wasn't rendered through concurrent mode
(
createRoot().render()
) then theSyncLane
is used. - If a render phase update, then the highest priority lane is used (or, the smallest Lane present in the given Lanes number 😉).
- One of the transition lanes if wrapped in
startTransition
- An event priority as seen in how root render schedule works
- If your application wasn't rendered through concurrent mode
(
Create an update object
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};The update is defined by many properties, but the only two relevant for now are the update
lane
and theaction
. React keeps track of them in order to be able to play them later. Of course, one other important property isnext
.. A linked list again.Enqueue a render phase update when called during render
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
}A render phase update is detected when the
fiber
is equal tocurrentlyRenderingFiber
or its alternate.When it is the case, React will call the
enqueueRenderPhaseUpdate
function which is implemented as follows:
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
): void {
// step 1
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
const pending = queue.pending;
if (pending === null) {
// step 2
update.next = update;
} else {
// step 3
update.next = pending.next;
pending.next = update;
}
// step 4
queue.pending = update;
}- Step 1: Mark as did schedule a render phase update. This variable is important for React since it will allow it to render the component again before going to other components or committing.
- Step 2: When there is no pending updates in the queue, the update
will point to itself as
next
- Step 3: When there are some pending updates, since it is a circular
linked list (meaning that the
pending
is the last item, andpending.next
is the first), then we will perform a small gymnastic to mark the first update as the next of the new update, and the new update the next of the first. - Step 4: Lately, React will mark the new
update
as thepending
of the queue (it is the last update to be performed.)
When not called during render, enqueue a concurrent hook update, then schedule an update on the fiber, then entangle transitions
if (false) {} else {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
}
}I won't explain this again to keep this section brief, all of this was explained during
how root.render() works
!. It works the same!At the end we will have a microtask scheduled to process the updateQueue of the fiber that caused the render.
Scheduling updates an efficient process, combined with the fast that react
combine fiber flags
of children into their parents, it knows for sure
when to start the work and when to stop.
The important things to note:
- React doesn't process the updates on the fly, but add them to the queue and schedules the work for later.
- React keeps track of the update details (the queue, the action/value/updater) so it can process them later.
- Queuing updates allows React to batch them and render one time rather than many.
Implementation on update
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
How useRef works
The [useRef
hook](https://react.dev/reference/react/useRef) gives you a
reference that you can control as you please. You can reference any javascript
value in it.
In the official docs, it is stated that:
useRef is a React Hook that lets you reference a value that’s not needed for rendering.
React tries to push you not to use this ref and base your decisions on it while the component is rendering. It is okay though to manipulate it outside render.
For example, if you mutate this value during render, React may render several times and thus write on it multiple times. The easiest way to observe this for example is by using a ref to count how many times your component rendered. In development mode and under strict mode, you will all the time get wrong results. That one reason to get away from it for this kind of usage.
People also often use the ref to detect whether the component is currently mounting or not, but this is also wrong: since if the component mounted already then suspended waiting for data while showing a Suspense fallback, the component stays mounted and thus any decision based on that value will be wrong.
You can also pass this ref object to any HTML element and React will attach the real DOM element on layout effect. Again, this is yet another reason to get away from this hook during render and minimize its usage.
Please refer to the official docs to learn more when to use and when not
to use the useRef
hook.
Signature
The useRef
hook is defined as follows:
function mountRef<T>(initialValue: T): {current: T} {
// [Not Native Code]
}
Implementation on mount
When your component using useRef
renders for the first time, it will call
the mountRef
function,
which is probably the easiest hook ever:
// simplified: dev warning about reading from ref during render were removed
// for clarity
function mountRef<T>(initialValue: T): {current: T} {
// step 1
const hook = mountWorkInProgressHook();
// step 2
const ref = {current: initialValue};
// step 3
hook.memoizedState = ref;
// step 4
return ref;
}
- Step 1: create the hook object on mount
- Step 2: create a javascript object with one
current
property, initialized by theinitialValue
received byuseRef
- Step 3: store this object as the
memoizedState
of this hook - Step 4: return the ref object
Implementation on update
On updates, useRef
will call the updateRef
function
which is very simple and doesn't need any explanations:
function updateRef<T>(initialValue: T): {current: T} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
It just creates the hook object while reusing the ref mutable object as is, and will return the memoized state directly.
How useState works
The useState
hook allows you to
add a variable to your component that it will cause it to update when its value
changes.
From the Official documentation:
useState is a React Hook that lets you add a state variable to your component.
State is the only reactive primitive in React for now (promises somehow are
reactive, but under the hood, they call scheduleUpdateOnFiber
too, and
it's kind hard to work only with them.).
The rest of this section assumes that you've read the useReducer
section.
Signature
useState
is defined as follows:
function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// [Not Native Code]
}
It accepts one single parameter called the initialState
which is the initial
state value of a function that will produce it.
Implementation on mount
Well, we'll have to spoil this already: useState
is useReducer
where the
reducer is:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
So, if you give useReducer
this reducer which, if it receives a function, it
will call it with the actual state, or else just take the passed value.
useReducer
will behave like useState
as we know it.
function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// step 1
const hook = mountStateImpl(initialState);
const queue = hook.queue;
// step 2
const dispatch: Dispatch<BasicStateAction<S>> = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
);
// step 3
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
- Step 1: The first step is where we will call
mountWIPHook
and initialize the hook object andUpdateQueue
. - Step 2: Reference the queue to use it while creating the
dispatch
function. - Step 3: Return the
useState
value which is a tuple of the state current value and the dispatch function (setState
).
How mountStateImpl
works
Let's take a look at how this function is implemented:
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
// step 1
const hook = mountWorkInProgressHook();
// step 2
if (typeof initialState === 'function') {
initialState = initialState();
}
// step 3
hook.memoizedState = hook.baseState = initialState;
// step 4
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;
}
- Step 1: Mount the hook object inside the linked list of hooks.
- Step 2: Call the initializer function if
initialState
is a function. - Step 3: Mark the hook's
memoizedState
as the initial value. Same forbaseState
. - Step 4: Create the update queue object and assign it to the hook's queue.
And that's it! useState is useReducer. Take a look at the commit where hooks were introduced back to 2018. It was literally:
export function useState<S>(
initialState: S | (() => S),
): [S, Dispatch<S, BasicStateAction<S>>] {
return useReducer(basicStateReducer, initialState);
}
Implementation on update
On updates, updateState
will delegate the work entirely to updateReducer
,
and that's it!
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
How useDebugValue works
This is an empty hook that only works in development mode.
The implementation of useDebugValue
is empty as shown in mountDebugValue
:
function mountDebugValue<T>(value: T, formatterFn?: (value: T) => mixed): void {
// This hook is normally a no-op.
// The react-debug-hooks package injects its own implementation
// so that e.g. DevTools can display custom hook values.
}
const updateDebugValue = mountDebugValue;
When taking a look at the ReactDebugHooks.js
file
we can see that this hook's implementation only pushes the logged value
to a module level array that collects information to be shown in the React
devtools, which is out of scope of this article and won't be an easy thing
to document for now.
How useDeferredValue works
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
How useTransition works
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
How useSyncExternalStore works
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
How useId works
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
Annex
This is a table for all the internal stored value for all hooks:
Hook | memoizedState | Comment(s) |
---|---|---|
use | N/A | use doesn't obey the rules of hooks and does not use the internal data structure |
useCallback | [callback, deps] | useCallback saves the passed callback and dependencies |
useContext | N/A | useContext doesn't rely on hooks call order, it is stored in the fiber.dependencies property |
useEffect | effect | useEffect saves the effect object created by pushEffect which references the effect function, the deps and so on |
useImperativeHandle | effect | useImperativeHandle calls is a useLayoutEffect under the hood |
useLayoutEffect | effect | useLayoutEffect saves the effect object created by pushEffect which references the effect function, the deps and so on |
useInsertionEffect | effect | useInsertionEffect saves the effect object created by pushEffect which references the effect function, the deps and so on |
useMemo | [value, deps] | useMemo saves the resulting cached value and the dependencies |
useReducer | state | useReducer saves only the state value in memoizedState, dispatch is stored in the queue |
useRef | {current: value} | useRef saves the {current: value} as memoized state |
useState | state | useState is useReducer with a special reducer, it saves only the state value in memoizedState, dispatch is stored in the queue |
useDebugValue | N/A | useDebugValue is an empty hook that is inject by the devtools |
useDeferredValue | TBD | |
useTransition | TBD | |
useSyncExternalStore | TBD | |
useId | TBD |