React Study Notes
Notes on React component design, render optimization, reconciliation and the role of key, plus the working mechanisms and usage caveats of common Hooks.
Updated
React Prerequisites
Major React Version Updates
- 16: became officially stable, with major-version compatibility
- 16.3–16.8: added lifecycle methods and hooks
- 17: changed event delegation from document to the root DOM container, and began progressive upgrades
- 18: fully automatic concurrent rendering, the
Suspensecomponent,startTransition, removed the event pool, async cleanup inuseEffect, andjsxmay no longer returnundefined
React Design Principles
- Declarative UI: the UI is determined by code declarations rather than manual runtime updates. All of React is one big
render, establishing the mappingUI = render(state)— new UI is mapped out as state changes - Unidirectional data flow: components are pure functions, passing data one way
- Virtual DOM:
- Decouples the underlying rendering, supporting other platforms such as Android, iOS, etc.
- Enables snapshot-like time-travel functionality
- Componentization: abstracts UI and logic for reuse and maintainability
React Component Objects
Take the following React component
const App = () => (
<div>
Component
</div>
)
Converted to js, it is:
const App = () => React.createElement(
// component name
'div',
// props
{},
// rest args as children
'Component',
)
After createElement executes, it is converted to:
const App = () => ({
$$typeof: Symbol(react.element),
ref: null,
key: null,
props: {
children: 'Component',
},
// For custom components, type is the function
type: 'div',
})
React Component Design
The Open-Closed Principle
The open-closed principle, in React terms, means "components should be easy to extend without changing their existing code".
Example
A common anti-pattern design looks like this:
const Button = ({ label, onClick, variant }: ButtonProps) => {
let className = "button"
if (variant === 'primary') {
className += ' button-primary'
} else if (variant === 'secondary') {
className += ' button-secondary'
} else if (variant === 'danger') {
className += ' button-danger'
}
return (
<button className={className} onClick={onClick}>
{label}
</button>
)
};
The main problems are:
- Supporting a new variant requires modifying the component
- The component needs to support every possible variant
- Every addition makes testing more complex
Refactored according to the open-closed principle:
type ButtonBaseProps = {
label: string
onClick: () => void
className?: string
children?: React.ReactNode
}
const ButtonBase = ({
label,
onClick,
className = '',
children,
}: ButtonBaseProps) => (
<button className={`button ${className}`.trim()} onClick={onClick}>
{children || label}
</button>
);
// Each variant gets its own component, reusing the base component
const PrimaryButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-primary" />
);
const SecondaryButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-secondary" />
);
const DangerButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-danger" />
);
The Composition Pattern
The example above abstracts downward — extracting a lower-level base component and forwarding its props — to achieve the open-closed principle.
But in business scenarios, cases where you can keep extracting downward are uncommon; more often the outer layer stays the same while the inner layer changes dynamically.
In React, the open-closed principle can also be achieved through the composition pattern and HOCs — that is, passing the child components that may change into the component. This approach is called the composition pattern.
type WithLoadingProps = {
isLoading?: boolean
}
const withLoading = <P extends object>(
WrappedComponent: React.ComponentType<P>
) => {
return ({ isLoading, ...props }: P & WithLoadingProps) => {
if (isLoading) {
return <div className="loading">Loading...</div>
}
return <WrappedComponent {...(props as P)} />
}
}
// Usage
const UserProfileWithLoading = withLoading(UserProfile)
Besides components, hooks can also be composable, for example:
const useDataFetching = <T>(url: string) => {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchData()
}, [url])
const fetchData = async () => {
try {
const response = await fetch(url)
const result = await response.json()
setData(result)
} catch (e) {
setError(e as Error)
} finally {
setLoading(false)
}
}
return { data, error, loading, refetch: fetchData }
}
// Extended without modification
const useUserData = (userId: string) => {
const result = useDataFetching<User>(`/api/users/${userId}`)
// Add user-specific functionality
const updateUser = async (data: Partial<User>) => {
// Update logic
}
return { ...result, updateUser }
}
Compared to extends
The React team's recommended "Composition vs Inheritance" explains the reasons in detail.
Reusing class components loses some extensibility.
From another angle, a class component is essentially a class with lifecycle and effects, while the composition pattern is a function with lifecycle and effects — the difference in granularity shows the difference in flexibility between the two.
Single Responsibility
Render Performance Optimization
re-render
The whole render is one big mapping system that handles the mapping UI = react(state).
So state is the trigger of re-render — every re-render is and can only be caused by a state change.
The re-render flow has a counterintuitive behavior: when a parent component re-renders, its child components re-render too, even if their state, props, etc. have not changed. To avoid this, React provides the React.memo mechanism.
Before understanding React.memo, we need to dig further into re-render — why does a parent re-render necessarily cause the child to re-render?
Although React advocates encapsulating components with pure-function thinking, React cannot guarantee every developer does so — for example, a component might get the time from Date.now() and render it.
When we insert a variable into jsx, React cannot determine whether the value changed since the last render, so it has to handle the problem pessimistically — re-rendering. Think of it as the pessimistic lock among concurrency locks.
Of course, you can tell React manually that a component is side-effect free — that is React.memo and React.PureComponent. These two work like the "sideEffect": false configuration in a cjs dependency package, helping the builder confirm this is a "pure" component/feature.
With React Devtools, enabling the Record why each component rendered while profiling. option highlights the components that triggered re-renders in the UI, which is very helpful for debugging.
React.memo
React.memo caches a component once; as long as props and state have not changed, the previous cache can keep being reused.
However, there is a hidden trap: when the parent component re-renders, its render function runs again, and under certain conditions this defeats React.memo. See the following example:
const PureBox = ({ boxes }) => {
return <ul>
{
boxes.map(box => <li>{ box.name }</li>)
}
</ul>
}
const Box = React.memo(PureBox)
const App = () => {
const [count, setCount] = useState()
const boxes = [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' },
]
return <>
<Box boxes={boxes} />
<h1>{ count }</h1>
<button onClick={() => setCount(count + 1)}>submit</button>
</>
}
Since arrays are reference types and this one is defined inside the render function, a re-render regenerates the boxes array — even if its values have not changed, its reference address has — so the Box component still re-renders.
To solve this, boxes needs to be cached too, so it is not recreated on every render. In rare cases we can extract constants into a separate file, but in most cases boxes does not stay the same forever — it changes under specific conditions — so we can use useMemo:
const PureBox = ({ boxes }) => {
return <ul>
{
boxes.map(box => <li>{ box.name }</li>)
}
</ul>
}
const Box = React.memo(PureBox)
const App = () => {
const [count, setCount] = useState()
const boxes = React.useMemo(() => [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' },
], [/* if memo uses state, declare it here, otherwise the value never changes */])
return <>
<Box boxes={boxes} />
<h1>{ count }</h1>
<button onClick={() => setCount(count + 1)}>submit</button>
</>
}
useMemo accepts a function as its argument and deals with objects and arrays.
But when you need to pass a function to a child component, using useMemo is awkward — you have to wrap it in another function, in the form React.useMemo(() => pruneFunction, []).
So React provides useCallback, used exactly like useMemo but acting on functions rather than objects and arrays. The example above can be rewritten as React.useCallback(pruneFunction, [])
useCallback is fairly common in custom hooks. See the following example:
function useToggle(initialValue) {
const [value, setValue] = React.useState(initialValue)
const toggle = React.useCallback(() => {
setValue(v => !v)
}, [])
return [value, toggle]
}
And when a component takes props, first consider whether the props need caching, for example:
const AuthContext = React.createContext({})
function AuthProvider({ user, status, forgotPwLink, children }){
const memoizedValue = React.useMemo(() => {
return {
user,
status,
forgotPwLink,
}
}, [user, status, forgotPwLink])
return (
<AuthContext.Provider value={memoizedValue}>
{children}
</AuthContext.Provider>
)
}
Summary
- If a component is pure, computationally expensive, or used in multiple places, caching with
React.memois recommended - If a reference value (array, function, object) is used by multiple components, caching with
useMemo/useCallbackis recommended - If a component is purely presentational, there is no need to cache it — unless analysis shows it is frequently re-rendered and needs manual handling
- Otherwise,
memocaching is not recommended: on one hand it costs extra memory; on the other, fewer renders may leave the view stale, producing unexpected display results
Reconciler
The reconciler is React's process of updating the DOM to match the component tree.
The old Reconciler used a stack architecture; the new version uses the fiber architecture. A comparison of the two can be seen at this online demo
In the stack architecture, the stack is a synchronous structure that blocks the thread while executing, and the stack must be emptied before the next work can continue — causing rendering jank (update time exceeding 1 / 60 s).
In the Fiber architecture, tasks are split into small phases, tasks that don't need to run immediately are made asynchronous, and pausing and resuming are supported.
With the Fiber architecture, Fiber divides the diff task into individual fiber units with assigned priorities: high-priority units are processed immediately (requestAnimationFrame), low-priority ones wait for idle time (requestIdleCallback). The whole Fiber can be viewed as an asynchronous virtual stack.
Fiber organizes the virtual DOM rendering by component into a linked list, which has next and sibling pointers — next points to the first child node, sibling to the next sibling node. So in the Fiber architecture, order matters.
After switching to an asynchronous architecture, the vDOM tree has an in-progress rendering phase — async tasks that have not finished executing. So during re-render, two copies of the vDOM exist; once Fiber finishes its tasks, the two vDOM trees are swapped. The vDOM tree is not actually entirely new — some nodes are still reused, such as nodes with the same key.
How It Works
The Reconciliation workflow is as follows:
- Call the component's render to create a new element tree
- Compare the new tree with the old tree
- Figure out which DOM operations are needed to update the old tree
- Perform the DOM operations efficiently
Terminology
re-render: calling the render function again, for comparing element trees.rebuild: rebuilding the element tree without any reuse.- element tree: think of it as the completed virtual DOM tree — from the Reconciler's perspective it is actually the element tree.
Comparison Principles
The comparison follows these principles:
- The element determines identity: only elements with the same
typeget patched; differenttypes are rebuilt directly- An HTML element's
typeis its tag name - A component's
typeis the component's function — for example, withconst Component = () => <div />,Component'stypeis theComponentfunction.
- An HTML element's
- Position matters: React watches whether a node at a given position switches type, so with conditionals:
{ condition ? <A /> : <B /> }: unmounts the original node and builds a new one to replace it{ condition ? <A propsA /> : <A propsB /> }: updates the component's props without destroying it
keycan replace position to identify a component: whenkeyis used, a component is identified as the same one and reused even if its position differs
key
According to the comparison principles above, key can replace the index to identify components, so when rendering lists, adding keys to child components whenever possible can effectively avoid rebuilds.
Besides lists, React does not require keys for static components, because their relative positions never change.
Dynamic keys, on the other hand, can effectively reorder components, for example:
const Component = () => {
const [isReverse, setIsReverse] = useState(false)
return (
<>
{/* The user's input can be moved into another component — that is, component reordering */}
<Input key={isReverse ? "some-key" : null} />
<Input key={!isReverse ? "some-key" : null} />
</>
)
}
However, because of positional identity, dynamic and static components are usually rendered together, for example:
<>
{items.map((item) => (
<ListItem key={item.id} />
))}
<StaticElement /> {/* Will this re-mount if items change? */}
</>
Newer versions of React optimized this structure — the code above is actually built as
[
// Dynamic components are merged through an extra wrapper layer to avoid affecting order
[
{ type: ListItem, key: "1" },
{ type: ListItem, key: "2" },
],
// The position becomes the fixed 2nd element
{ type: StaticElement },
]
React Component Development Recommendations
Don't Use Inline Components
const Parent = () => {
// InnerComponent's type is updated on every render call (not the same reference)
// so every render call recreates InnerComponent, causing significant performance loss
const InnerComponent = () => <div>Inner content</div>
return <InnerComponent />
}
Compose Whenever Possible
const CounterButton = () => {
// When the state updates, only CounterButton needs to update
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}
const Parent = () => {
return (
<div>
<CounterButton />
<ExpensiveComponent />
</div>
)
}
Reuse Components via key
Among the ways to identify a component, key has the highest priority — even if type and order both differ, the same key means the same component.
In the code below, React treats these as the same component whose type changed, rather than different components.
const TabContent = ({ activeTab }) => {
// All tabs have the same key, so switching tabs does not rebuild, even though the component types differ.
return (
<div>
{activeTab === "profile" && <ProfileTab key="tab-content" />}
{activeTab === "settings" && <SettingsTab key="tab-content" />}
{activeTab === "activity" && <ActivityTab key="tab-content" />}
</div>
)
}
This way, cached values of native DOM elements like input inside the component are preserved too, even though it is not the same component.
Isolate State-Unrelated Components
That is the state colocation pattern — keep components as close as possible to where they are used. See the following code:
const App = () => {
const [filterText, setFilterText] = useState("")
const filteredUsers = users.filter((user) => user.name.includes(filterText))
return (
<>
<SearchBox filterText={filterText} onChange={setFilterText} />
<UserList users={filteredUsers} />
{/* When filterText changes, ExpensiveComponent also re-renders */}
<ExpensiveComponent />
</>
)
}
ExpensiveComponent is actually unrelated to the state; the state and its dependent components can be isolated into a child component to reduce re-renders
const UserSection = () => {
const [filterText, setFilterText] = useState("")
const filteredUsers = users.filter((user) => user.name.includes(filterText))
return (
<>
<SearchBox filterText={filterText} onChange={setFilterText} />
<UserList users={filteredUsers} />
</>
)
}
const App = () => {
return (
<>
<UserSection />
<ExpensiveComponent />
</>
)
}
Component Design
If a component is inefficient and slow to render, consider whether the design itself can be further optimized:
- Does it have a single responsibility?
- Is the state isolated?
Only if both are satisfied should you consider caching with memo.
Reconciliation Clean Architecture
Design principles in Reconciliation:
- Single responsibility: each component should have one reason to update — for example, a change in some state.
- Dependency inversion: components should depend on abstractions rather than concrete implementations, which makes performance optimization through composition easier.
- Interface segregation: components should have minimal, focused interfaces, reducing the chances that props changes trigger re-renders.
hooks
useState
useState is the most basic Hook in React; it lets us add state to function components. Its working principle is as follows:
Data Structure
- React uses the Fiber architecture, where each component corresponds to a Fiber node
- Each Fiber node has a
memoizedStateproperty that stores the hooks linked list - Each hook is an object containing
memoizedState(the current state value) andnext(pointing to the next hook)
Implementation Principle
// Pseudocode implementation
let currentHook = null; // The hook currently being processed
let workInProgressHook = null; // The hook being built
let isMount = true; // Whether this is the first render
function useState(initialState) {
// Get the current hook
let hook;
if (isMount) {
// First render — create a new hook
hook = {
memoizedState: initialState,
next: null,
queue: { pending: null }
};
if (!workInProgressHook) {
workInProgressHook = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
// Get the current state
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = typeof action === 'function' ? action(baseState) : action;
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending.next);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function dispatchAction(queue, action) {
const update = { action, next: null };
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
scheduleWork();
}
Workflow
- On the first render, React creates the hooks linked list in order
- Each useState call takes the corresponding hook from the list
- Calling setState adds the update to the hook's update queue
- React processes the update queue in batches at the right time (e.g. after event handling completes)
- When processing updates, the updates in the queue are executed in order to produce the final state
Caveats
- Hooks must be called at the top level of a function component — not inside conditions, loops, or nested functions
- This is because React relies on the call order of hooks to correctly associate state
- The call order of hooks must stay consistent across renders
- If you need state inside a condition, put the conditional logic inside the hook
useEffect
useEffect is React's Hook for handling side effects; it lets us perform side-effect operations in function components (such as data fetching, subscriptions, manual DOM modification, etc.). Its working principle is as follows:
Data Structure
- The hook object for each useEffect contains:
memoizedState: stores the effect objectnext: points to the next hook
- The effect object contains:
create: the side-effect functiondestroy: the cleanup functiondeps: the dependency arraynext: points to the next effect
Implementation Principle
// Pseudocode implementation
function useEffect(create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// Mark the current hook as an effect hook
hook.memoizedState = pushEffect(
HookHasEffect | HookPassive,
create,
undefined,
nextDeps,
);
}
function pushEffect(tag, create, destroy, deps) {
const effect = {
tag,
create,
destroy,
deps,
next: null,
};
// Add the effect to the update queue
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
// Execute effects in the commit phase
function commitHookEffectList(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Execute the effect
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
Workflow
- On mount:
- Create the effect object
- Add the effect to the update queue
- Execute the effect in the commit phase
- On update:
- Compare whether the dependency array changed
- If the dependencies changed, run the cleanup function first, then run the new effect
- If the dependencies did not change, skip the effect
- On unmount:
- Run the cleanup functions of all effects
Caveats
- useEffect runs asynchronously after the browser finishes rendering
- The cleanup function runs when the component unmounts or the dependencies change
- With an empty dependency array, the effect only runs on mount and unmount
- With no dependency array provided, the effect runs after every render
- You can return a cleanup function from an effect to clean up side effects
- Do not perform render-blocking operations inside an effect
Differences from useLayoutEffect
- useEffect runs asynchronously after the browser finishes rendering
- useLayoutEffect runs synchronously after DOM updates
- useLayoutEffect blocks browser rendering
- useEffect should be used in most cases
- Only use useLayoutEffect when you need to read the DOM layout synchronously
useLayoutEffect
useLayoutEffect is React's Hook for handling side effects; it lets us perform side-effect operations in function components (such as data fetching, subscriptions, manual DOM modification, etc.). Its working principle is as follows:
Data Structure
- The hook object for each useLayoutEffect contains:
memoizedState: stores the effect objectnext: points to the next hook
- The effect object contains:
create: the side-effect functiondestroy: the cleanup functiondeps: the dependency arraynext: points to the next effect
Implementation Principle
// Pseudocode implementation
function useLayoutEffect(create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// Mark the current hook as an effect hook
hook.memoizedState = pushEffect(
HookHasEffect | HookLayout,
create,
undefined,
nextDeps,
);
}
function pushEffect(tag, create, destroy, deps) {
const effect = {
tag,
create,
destroy,
deps,
next: null,
};
// Add the effect to the update queue
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
// Execute effects in the commit phase
function commitHookEffectList(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Execute the effect
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
Workflow
- On mount:
- Create the effect object
- Add the effect to the update queue
- Execute the effect in the commit phase
- On update:
- Compare whether the dependency array changed
- If the dependencies changed, run the cleanup function first, then run the new effect
- If the dependencies did not change, skip the effect
- On unmount:
- Run the cleanup functions of all effects
Caveats
- useLayoutEffect runs synchronously after DOM updates
- useLayoutEffect blocks browser rendering
- Only use useLayoutEffect when you need to read the DOM layout synchronously
useCallback
useCallback is React's Hook for caching callback functions; it lets us cache callback functions in function components. Its working principle is as follows:
Data Structure
- The hook object for each useCallback contains:
memoizedState: stores the callback functionnext: points to the next hook
- The callback function object contains:
deps: the dependency arraynext: points to the next callback function
Implementation Principle
// Pseudocode implementation
function useCallback(callback, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// Mark the current hook as a callback hook
hook.memoizedState = {
deps: nextDeps,
next: callback,
};
}
Workflow
- On the first render, React creates the hooks linked list in order
- Each useCallback call takes the corresponding hook from the list
- If the dependency array has not changed, the cached callback is returned
- If the dependency array has changed, a new callback is returned
Caveats
- useCallback creates the callback function on the first render
- If you need a callback inside a condition, put the conditional logic inside the hook
useMemo
useMemo is React's Hook for caching computation results; it lets us cache computation results in function components. Its working principle is as follows:
Data Structure
- The hook object for each useMemo contains:
memoizedState: stores the computation resultnext: points to the next hook
- The computation result object contains:
deps: the dependency arraynext: points to the next computation result
Implementation Principle
// Pseudocode implementation
function useMemo(create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// Mark the current hook as a memo hook
hook.memoizedState = {
deps: nextDeps,
next: create(),
};
}
Workflow
- On the first render, React creates the hooks linked list in order
- Each useMemo call takes the corresponding hook from the list
- If the dependency array has not changed, the cached computation result is returned
- If the dependency array has changed, the result is recomputed and the new result is returned
Caveats
- useMemo creates the computation result on the first render
- If you need a computation result inside a condition, put the conditional logic inside the hook
useRef
useRef is React's Hook for creating mutable references; it lets us create mutable references in function components. Its working principle is as follows:
Data Structure
- The hook object for each useRef contains:
memoizedState: stores the reference objectnext: points to the next hook
- The reference object contains:
current: stores the reference value
Implementation Principle
// Pseudocode implementation
function useRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };
hook.memoizedState = ref;
return ref;
}
Workflow
- On the first render, React creates the hooks linked list in order
- Each useRef call takes the corresponding hook from the list
- Returns a new reference object
Caveats
- useRef creates the reference object on the first render
- If you need a reference inside a condition, put the conditional logic inside the hook
useTransition
useTransition is React's Hook for handling transition states; it lets us handle transition states in function components. Its working principle is as follows:
Data Structure
- The hook object for each useTransition contains:
memoizedState: stores the transition state objectnext: points to the next hook
- The transition state object contains:
isPending: whether a transition is in progressstartTransition: the function to start a transition
Implementation Principle
// Pseudocode implementation
function useTransition() {
const hook = mountWorkInProgressHook();
const transition = {
isPending: false,
startTransition,
};
hook.memoizedState = transition;
return transition;
}
function startTransition(callback) {
// Implement the transition logic
}
Workflow
- On the first render, React creates the hooks linked list in order
- Each useTransition call takes the corresponding hook from the list
- Returns a new transition state object
Caveats
- useTransition creates the transition state object on the first render
- If you need a transition state inside a condition, put the conditional logic inside the hook
useReducer
useReducer is React's Hook for managing state; it lets us manage state in function components. Its working principle is as follows:
Data Structure
- The hook object for each useReducer contains:
memoizedState: stores the statenext: points to the next hook
- The state object contains:
reducer: the state management functionstate: stores the state
Implementation Principle
// Pseudocode implementation
function useReducer(reducer, initialState) {
const hook = mountWorkInProgressHook();
const state = {
reducer,
state: initialState,
};
hook.memoizedState = state;
return state;
}
Workflow
- On the first render, React creates the hooks linked list in order
- Each useReducer call takes the corresponding hook from the list
- Returns a new state object
Caveats
- useReducer creates the state object on the first render
- If you need state inside a condition, put the conditional logic inside the hook
useContext
useContext is React's Hook for accessing context; it lets us access context in function components. Its working principle is as follows:
Data Structure
- The hook object for each useContext contains:
memoizedState: stores the context objectnext: points to the next hook
- The context object contains:
context: stores the context
Implementation Principle
// Pseudocode implementation
function useContext(context) {
const hook = mountWorkInProgressHook();
const contextValue = React.useContext(context);
hook.memoizedState = contextValue;
return contextValue;
}
Workflow
- On the first render, React creates the hooks linked list in order
- Each useContext call takes the corresponding hook from the list
- Returns the context object
Caveats
- useContext creates the context object on the first render
- If you need context inside a condition, put the conditional logic inside the hook