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:

  1. Don't call hooks in conditions/loops — the linked list position would change between renders, causing hooks to mismatch their state
  2. Only call hooks at the top level — ensures consistent ordering every render

useState internals:

  • First render: creates a Hook node, stores initial state
  • Updates: setState enqueues an update to hook.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 deps with previous deps (Object.is comparison)
  • If deps changed: runs destroy (cleanup), then runs create
  • Timing: useEffect fires asynchronously after paint; useLayoutEffect fires 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

codeTap to expand ⛶
1// === HOOKS LINKED LIST VISUALIZATION ===
2
3function MyComponent() {
4 // Hook 1: useState → memoizedState: 0
5 const [count, setCount] = useState(0);
6
7 // Hook 2: useState → memoizedState: ''
8 const [name, setName] = useState('');
9
10 // Hook 3: useMemo → memoizedState: [computedValue, [count]]
11 const doubled = useMemo(() => count * 2, [count]);
12
13 // Hook 4: useCallback → memoizedState: [fn, [count]]
14 const increment = useCallback(() => setCount(c => c + 1), []);
15
16 // 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]);
21
22 // Internal linked list:
23 // Fiber.memoizedState → [0, setter] → ['', setter] → [0, [0]]
24 // → [fn, []] → {effect, [0]} → null
25}
26
27// === ADVANCED HOOK PATTERNS ===
28
29// 1. useReducer for complex state machines
30type AuthState =
31 | { status: 'idle' }
32 | { status: 'authenticating' }
33 | { status: 'authenticated'; user: User; token: string }
34 | { status: 'error'; error: string; retryCount: number };
35
36type AuthAction =
37 | { type: 'LOGIN_START' }
38 | { type: 'LOGIN_SUCCESS'; user: User; token: string }
39 | { type: 'LOGIN_FAILURE'; error: string }
40 | { type: 'LOGOUT' };
41
42function 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}
58
59// 2. Custom hook with cleanup and race condition prevention
60function 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 });
66
67 useEffect(() => {
68 let cancelled = false; // Race condition guard
69
70 setState(prev => ({ ...prev, loading: true, error: null }));
71
72 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 });
79
80 return () => { cancelled = true; }; // Prevent stale updates
81 }, deps);
82
83 return state;
84}
85
86// 3. useRef for mutable values that don't trigger re-renders
87function useInterval(callback: () => void, delay: number | null) {
88 const savedCallback = useRef(callback);
89
90 // Update ref to latest callback without re-creating interval
91 useEffect(() => {
92 savedCallback.current = callback;
93 }, [callback]);
94
95 useEffect(() => {
96 if (delay === null) return;
97
98 const id = setInterval(() => savedCallback.current(), delay);
99 return () => clearInterval(id);
100 }, [delay]); // Only recreate interval when delay changes
101}
102
103// 4. Avoiding the stale closure problem with refs
104function 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 render
110}

🏋️ Practice Exercise

Hooks Internals Exercises:

  1. Break the rules of hooks on purpose (call useState inside a condition) and observe the error
  2. Implement a useForceUpdate hook that triggers a re-render without any state change
  3. Build a useDebounce hook that debounces a value with configurable delay
  4. Create a useWhyDidYouRender hook that logs which props/state changed between renders
  5. Implement usePrevious hook and explain WHY it works (hint: useEffect timing)
  6. Build a state machine hook useMachine that enforces valid state transitions

⚠️ Common Mistakes

  • Calling hooks conditionally — if (condition) { useState() } breaks the linked list ordering and causes hooks to mismatch

  • Missing 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.