JavaScript Development Space

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:

  1. create (Function): Executed during the first render and subsequent updates.
  2. deps (Array): Dependency array that determines when create should be re-executed.

Example

jsx
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.
8
9 return <h1>Hello World</h1>;
10 }
11
12 function App() {
13 const [visible, setVisible] = useState(true);
14
15 useEffect(() => {
16 console.log('App Mounted');
17 }, []); // Runs only once during mount.
18
19 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.

js
1 function Hook() {
2 this.memoizedState = null; // Stores hook state
3 this.next = null; // Points to the next hook
4 this.queue = []; // Stores state update functions
5 }

2.2 Extending FiberNode

A FiberNode object tracks updates for each component instance, including hook-related information.

js
1 function FiberNode() {
2 this.updateQueue = null; // Stores `useEffect` data
3 }

2.3 Differentiating Effect Types

HookFlags distinguish between effect types like useEffect and useLayoutEffect.

js
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:

  1. A new Hook object is created.
  2. Effect objects store the create function, dependencies (deps), and cleanup function (destroy).
  3. Effects are pushed into the updateQueue.

Key implementation:

js
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 }
10
11 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 }
18
19 function mountEffect(create, deps) {
20 const hook = mountWorkInProgressHook();
21 hook.memoizedState = pushEffect(HookPassive | HookHasEffect, create, deps);
22 }

4. Updating useEffect

During updates:

  1. Old hooks are reused, and deps are compared using Object.is.
  2. If dependencies differ, the create function is re-executed.

Key implementation:

js
1 function updateWorkInProgressHook() {
2 currentHook = currentHook ? currentHook.next : currentlyRenderingFiber.alternate.memoizedState;
3
4 const hook = new Hook();
5 hook.memoizedState = currentHook.memoizedState;
6 hook.queue = currentHook.queue;
7
8 if (!workInProgressHook) {
9 currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
10 } else {
11 workInProgressHook = workInProgressHook.next = hook;
12 }
13 return hook;
14 }
15
16 function updateEffect(create, deps) {
17 const hook = updateWorkInProgressHook();
18 const prevEffect = hook.memoizedState;
19
20 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:

js
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:

js
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:

js
1 // Define Hook and FiberNode prototypes
2 function Hook() {
3 this.memoizedState = null; // Hook state
4 this.next = null; // Pointer to the next Hook
5 this.queue = []; // State update queue
6 }
7
8 function FiberNode() {
9 this.updateQueue = null; // Effect queue
10 }
11
12 // Hook flags for effect types
13 const HookHasEffect = 1;
14 const HookInsertion = 2; // For `useInsertionEffect`
15 const HookLayout = 4; // For `useLayoutEffect`
16 const HookPassive = 8; // For `useEffect`
17
18 // Global variables for managing fiber nodes and hooks
19 let currentlyRenderingFiber = null;
20 let currentHook = null;
21 let workInProgressHook = null;
22
23 // Helper functions
24 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 }
31
32 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 }
39
40 // Mounting hooks and effects
41 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 }
50
51 function mountEffect(create, deps) {
52 const hook = mountWorkInProgressHook();
53 hook.memoizedState = pushEffect(HookPassive | HookHasEffect, create, deps);
54 }
55
56 // Updating hooks and effects
57 function updateWorkInProgressHook() {
58 currentHook = currentHook ? currentHook.next : currentlyRenderingFiber.alternate.memoizedState;
59
60 const hook = new Hook();
61 hook.memoizedState = currentHook.memoizedState;
62 hook.queue = currentHook.queue;
63
64 if (!workInProgressHook) {
65 currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
66 } else {
67 workInProgressHook = workInProgressHook.next = hook;
68 }
69 return hook;
70 }
71
72 function updateEffect(create, deps) {
73 const hook = updateWorkInProgressHook();
74 const prevEffect = hook.memoizedState;
75
76 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 }
83
84 // Cleanup logic
85 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 }
96
97 // Main `useEffect` implementation
98 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.

JavaScript Development Space

© 2025 JavaScript Development Space - Master JS and NodeJS. All rights reserved.