Hooks Internals & Advanced Patterns
📖 Concept
Hooks are implemented as a linked list on each Fiber node. When you call useState, useEffect, etc., React doesn't use the hook name — it uses call order to match hooks to their state.
How hooks work internally:
FiberNode.memoizedState → Hook1 → Hook2 → Hook3 → null
(useState) (useEffect) (useMemo)
Each hook is a node in a linked list:
Hook {
memoizedState: any, // The state value (useState) or effect (useEffect)
queue: UpdateQueue, // Pending state updates
next: Hook | null, // Next hook in the list
}
This is why hooks rules exist:
- Don't call hooks in conditions/loops — the linked list position would change between renders, causing hooks to mismatch their state
- Only call hooks at the top level — ensures consistent ordering every render
useState internals:
- First render: creates a Hook node, stores initial state
- Updates:
setStateenqueues an update tohook.queue - Next render: React processes the update queue and produces new state
- Batching: Multiple setStates in the same event handler create multiple updates but trigger ONE re-render (React 18+: automatic batching everywhere)
useEffect internals:
- Creates an Effect object:
{ tag, create, destroy, deps, next } - After commit phase, React iterates the effect list
- Compares
depswith previousdeps(Object.iscomparison) - If deps changed: runs
destroy(cleanup), then runscreate - Timing:
useEffectfires asynchronously after paint;useLayoutEffectfires synchronously before paint
useMemo/useCallback internals:
- Stores
[value, deps]in the hook's memoizedState - On re-render, compares new deps with stored deps
- If same: returns stored value (no recomputation)
- If different: recomputes and stores new value + deps
- Important: deps comparison is shallow (
Object.is) — objects/arrays are compared by reference, not value
💻 Code Example
1// === HOOKS LINKED LIST VISUALIZATION ===23function MyComponent() {4 // Hook 1: useState → memoizedState: 05 const [count, setCount] = useState(0);67 // Hook 2: useState → memoizedState: ''8 const [name, setName] = useState('');910 // Hook 3: useMemo → memoizedState: [computedValue, [count]]11 const doubled = useMemo(() => count * 2, [count]);1213 // Hook 4: useCallback → memoizedState: [fn, [count]]14 const increment = useCallback(() => setCount(c => c + 1), []);1516 // Hook 5: useEffect → memoizedState: { create, destroy, deps: [count] }17 useEffect(() => {18 console.log('Count changed:', count);19 return () => console.log('Cleanup for count:', count);20 }, [count]);2122 // Internal linked list:23 // Fiber.memoizedState → [0, setter] → ['', setter] → [0, [0]]24 // → [fn, []] → {effect, [0]} → null25}2627// === ADVANCED HOOK PATTERNS ===2829// 1. useReducer for complex state machines30type AuthState =31 | { status: 'idle' }32 | { status: 'authenticating' }33 | { status: 'authenticated'; user: User; token: string }34 | { status: 'error'; error: string; retryCount: number };3536type AuthAction =37 | { type: 'LOGIN_START' }38 | { type: 'LOGIN_SUCCESS'; user: User; token: string }39 | { type: 'LOGIN_FAILURE'; error: string }40 | { type: 'LOGOUT' };4142function authReducer(state: AuthState, action: AuthAction): AuthState {43 switch (action.type) {44 case 'LOGIN_START':45 return { status: 'authenticating' };46 case 'LOGIN_SUCCESS':47 return { status: 'authenticated', user: action.user, token: action.token };48 case 'LOGIN_FAILURE':49 return {50 status: 'error',51 error: action.error,52 retryCount: state.status === 'error' ? state.retryCount + 1 : 1,53 };54 case 'LOGOUT':55 return { status: 'idle' };56 }57}5859// 2. Custom hook with cleanup and race condition prevention60function useAsyncData<T>(fetchFn: () => Promise<T>, deps: any[]) {61 const [state, setState] = useState<{62 data: T | null;63 error: Error | null;64 loading: boolean;65 }>({ data: null, error: null, loading: true });6667 useEffect(() => {68 let cancelled = false; // Race condition guard6970 setState(prev => ({ ...prev, loading: true, error: null }));7172 fetchFn()73 .then(data => {74 if (!cancelled) setState({ data, error: null, loading: false });75 })76 .catch(error => {77 if (!cancelled) setState({ data: null, error, loading: false });78 });7980 return () => { cancelled = true; }; // Prevent stale updates81 }, deps);8283 return state;84}8586// 3. useRef for mutable values that don't trigger re-renders87function useInterval(callback: () => void, delay: number | null) {88 const savedCallback = useRef(callback);8990 // Update ref to latest callback without re-creating interval91 useEffect(() => {92 savedCallback.current = callback;93 }, [callback]);9495 useEffect(() => {96 if (delay === null) return;9798 const id = setInterval(() => savedCallback.current(), delay);99 return () => clearInterval(id);100 }, [delay]); // Only recreate interval when delay changes101}102103// 4. Avoiding the stale closure problem with refs104function usePrevious<T>(value: T): T | undefined {105 const ref = useRef<T>();106 useEffect(() => {107 ref.current = value;108 });109 return ref.current; // Returns value from PREVIOUS render110}
🏋️ Practice Exercise
Hooks Internals Exercises:
- Break the rules of hooks on purpose (call useState inside a condition) and observe the error
- Implement a
useForceUpdatehook that triggers a re-render without any state change - Build a
useDebouncehook that debounces a value with configurable delay - Create a
useWhyDidYouRenderhook that logs which props/state changed between renders - Implement
usePrevioushook and explain WHY it works (hint: useEffect timing) - Build a state machine hook
useMachinethat enforces valid state transitions
⚠️ Common Mistakes
Calling hooks conditionally —
if (condition) { useState() }breaks the linked list ordering and causes hooks to mismatchMissing dependencies in useEffect — leads to stale closures where the effect uses outdated values
Over-specifying dependencies — adding objects/arrays as deps that are recreated every render, causing infinite effect loops
Using useCallback/useMemo everywhere 'for performance' — the memoization overhead (storing deps, comparing) can exceed the cost of just re-computing
Not using the functional updater form of setState —
setCount(count + 1)captures stale count;setCount(c => c + 1)always has latest
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Hooks Internals & Advanced Patterns. Login to unlock this feature.