How to Write React's useEffect and Understand Its Principle
This article explains how to implement useEffect
manually in React to understand its inner workings. It includes the method's parameters, its execution logic during the first and subsequent renders, and the core principles behind useEffect
. Topics include the creation of hooks, managing dependencies, and lifecycle-like behavior with useEffect
. Code examples illustrate key concepts, and reference materials are provided.
1. Introduction to useEffect
The useEffect
method accepts two parameters:
create
(Function): Executed during the first render and subsequent updates.deps
(Array): Dependency array that determines when create should be re-executed.
Example
1 function HelloWorld() {2 useEffect(() => {3 console.log('HelloWorld Mounted');4 return () => {5 console.log('HelloWorld Unmounted');6 };7 }, []); // Empty deps array ensures this runs only on mount and unmount.89 return <h1>Hello World</h1>;10 }1112 function App() {13 const [visible, setVisible] = useState(true);1415 useEffect(() => {16 console.log('App Mounted');17 }, []); // Runs only once during mount.1819 return (20 <div>21 <button onClick={() => setVisible(!visible)}>Toggle HelloWorld</button>22 {visible && <HelloWorld />}23 </div>24 );25 }
Output:
- First render: "HelloWorld Mounted"
- Toggling the button: "HelloWorld Unmounted" and "HelloWorld Mounted"
Implementing useEffect
2.1 Creating a Hook Object
A Hook object stores hook-specific data and is linked in a chain for function components.
1 function Hook() {2 this.memoizedState = null; // Stores hook state3 this.next = null; // Points to the next hook4 this.queue = []; // Stores state update functions5 }
2.2 Extending FiberNode
A FiberNode
object tracks updates for each component instance, including hook-related information.
1 function FiberNode() {2 this.updateQueue = null; // Stores `useEffect` data3 }
2.3 Differentiating Effect Types
HookFlags
distinguish between effect types like useEffect
and useLayoutEffect
.
1 const HookHasEffect = 1;2 const HookInsertion = 2; // For `useInsertionEffect`3 const HookLayout = 4; // For `useLayoutEffect`4 const HookPassive = 8; // For `useEffect`
3. First Call to useEffect
When useEffect
is called for the first time:
- A new Hook object is created.
- Effect objects store the
create
function, dependencies (deps
), and cleanup function (destroy
). - Effects are pushed into the
updateQueue
.
Key implementation:
1 function mountWorkInProgressHook() {2 const hook = new Hook();3 if (!workInProgressHook) {4 currentlyRenderingFiber.memoizedState = workInProgressHook = hook;5 } else {6 workInProgressHook = workInProgressHook.next = hook;7 }8 return hook;9 }1011 function pushEffect(tag, create, deps, destroy = null) {12 const effect = { tag, create, deps, destroy };13 if (!currentlyRenderingFiber.updateQueue) {14 currentlyRenderingFiber.updateQueue = [];15 }16 currentlyRenderingFiber.updateQueue.push(effect);17 }1819 function mountEffect(create, deps) {20 const hook = mountWorkInProgressHook();21 hook.memoizedState = pushEffect(HookPassive | HookHasEffect, create, deps);22 }
4. Updating useEffect
During updates:
- Old hooks are reused, and
deps
are compared usingObject.is
. - If dependencies differ, the
create
function is re-executed.
Key implementation:
1 function updateWorkInProgressHook() {2 currentHook = currentHook ? currentHook.next : currentlyRenderingFiber.alternate.memoizedState;34 const hook = new Hook();5 hook.memoizedState = currentHook.memoizedState;6 hook.queue = currentHook.queue;78 if (!workInProgressHook) {9 currentlyRenderingFiber.memoizedState = workInProgressHook = hook;10 } else {11 workInProgressHook = workInProgressHook.next = hook;12 }13 return hook;14 }1516 function updateEffect(create, deps) {17 const hook = updateWorkInProgressHook();18 const prevEffect = hook.memoizedState;1920 if (deps && areHookInputsEqual(deps, prevEffect.deps)) {21 hook.memoizedState = pushEffect(HookPassive, create, deps, prevEffect.destroy);22 } else {23 currentlyRenderingFiber.flags |= HookPassive;24 hook.memoizedState = pushEffect(HookPassive | HookHasEffect, create, deps, prevEffect.destroy);25 }26 }
5. Managing Cleanup
Clean up previous effects during unmount or dependency changes:
1 function commitHookPassiveUnmountEffects(fiber, hookFlags) {2 const queue = fiber.updateQueue;3 queue.forEach((effect) => {4 if (effect.tag & hookFlags && effect.destroy) {5 effect.destroy();6 effect.destroy = null;7 }8 });9 }
6. Final useEffect Implementation
The final implementation switches between mountEffect
and updateEffect
:
1 function useEffect(create, deps) {2 const current = currentlyRenderingFiber.alternate;3 if (!current) {4 mountEffect(create, deps);5 } else {6 updateEffect(create, deps);7 }8 }
7. Full Code Implementation
Below is the complete code implementation for useEffect
, including mount, update, and cleanup logic:
1 // Define Hook and FiberNode prototypes2 function Hook() {3 this.memoizedState = null; // Hook state4 this.next = null; // Pointer to the next Hook5 this.queue = []; // State update queue6 }78 function FiberNode() {9 this.updateQueue = null; // Effect queue10 }1112 // Hook flags for effect types13 const HookHasEffect = 1;14 const HookInsertion = 2; // For `useInsertionEffect`15 const HookLayout = 4; // For `useLayoutEffect`16 const HookPassive = 8; // For `useEffect`1718 // Global variables for managing fiber nodes and hooks19 let currentlyRenderingFiber = null;20 let currentHook = null;21 let workInProgressHook = null;2223 // Helper functions24 function areHookInputsEqual(nextDeps, prevDeps) {25 if (!nextDeps || !prevDeps) return false;26 for (let i = 0; i < nextDeps.length; i++) {27 if (!Object.is(nextDeps[i], prevDeps[i])) return false;28 }29 return true;30 }3132 function pushEffect(tag, create, deps, destroy = null) {33 const effect = { tag, create, deps, destroy };34 if (!currentlyRenderingFiber.updateQueue) {35 currentlyRenderingFiber.updateQueue = [];36 }37 currentlyRenderingFiber.updateQueue.push(effect);38 }3940 // Mounting hooks and effects41 function mountWorkInProgressHook() {42 const hook = new Hook();43 if (!workInProgressHook) {44 currentlyRenderingFiber.memoizedState = workInProgressHook = hook;45 } else {46 workInProgressHook = workInProgressHook.next = hook;47 }48 return hook;49 }5051 function mountEffect(create, deps) {52 const hook = mountWorkInProgressHook();53 hook.memoizedState = pushEffect(HookPassive | HookHasEffect, create, deps);54 }5556 // Updating hooks and effects57 function updateWorkInProgressHook() {58 currentHook = currentHook ? currentHook.next : currentlyRenderingFiber.alternate.memoizedState;5960 const hook = new Hook();61 hook.memoizedState = currentHook.memoizedState;62 hook.queue = currentHook.queue;6364 if (!workInProgressHook) {65 currentlyRenderingFiber.memoizedState = workInProgressHook = hook;66 } else {67 workInProgressHook = workInProgressHook.next = hook;68 }69 return hook;70 }7172 function updateEffect(create, deps) {73 const hook = updateWorkInProgressHook();74 const prevEffect = hook.memoizedState;7576 if (deps && areHookInputsEqual(deps, prevEffect.deps)) {77 hook.memoizedState = pushEffect(HookPassive, create, deps, prevEffect.destroy);78 } else {79 currentlyRenderingFiber.flags |= HookPassive;80 hook.memoizedState = pushEffect(HookPassive | HookHasEffect, create, deps, prevEffect.destroy);81 }82 }8384 // Cleanup logic85 function commitHookPassiveUnmountEffects(fiber, hookFlags) {86 const queue = fiber.updateQueue;87 if (queue) {88 queue.forEach((effect) => {89 if (effect.tag & hookFlags && effect.destroy) {90 effect.destroy();91 effect.destroy = null;92 }93 });94 }95 }9697 // Main `useEffect` implementation98 function useEffect(create, deps) {99 const current = currentlyRenderingFiber.alternate;100 if (!current) {101 mountEffect(create, deps);102 } else {103 updateEffect(create, deps);104 }105 }
References
React useEffect Official Documentation
Conclusion
By implementing useEffect
from scratch, we gain a deeper understanding of its underlying mechanics, including how hooks manage dependencies, execute side effects, and handle cleanup operations. This exercise provides valuable insights into React's rendering process, fiber architecture, and lifecycle management. Whether you’re troubleshooting useEffect
in your projects or striving to master React’s inner workings, this knowledge equips you with the tools to write more efficient and predictable code.