React: Multiple useEffect
Hooks with []
Empty Dependency: Promises vs setTimeout
Understanding Multiple useEffect
with Empty Dependencies
React’s useEffect
with an empty dependency array ([]
) is a familiar pattern for running code after the component mounts. But what happens when you use multiple useEffect
hooks—one containing a promise and another with setTimeout
? The interaction can get tricky, especially when understanding the execution order.
Let’s explore various combinations to clarify how React handles them.
Example 1: Promise vs setTimeout
import React, { useEffect } from "react";
function App() {
useEffect(() => {
console.log("Effect 1: Promise starts");
(async () => {
await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulate async task
console.log("Effect 1: Promise resolved");
})();
}, []); // Runs on mount
useEffect(() => {
console.log("Effect 2: setTimeout scheduled");
setTimeout(() => {
console.log("Effect 2: setTimeout executed");
}, 0); // Minimal delay
}, []); // Runs on mount
return <div>Check the console!</div>;
}
Effect 1: Promise starts
Effect 2: setTimeout scheduled
Effect 2: setTimeout executed
Effect 1: Promise resolved
Explanation:
- Effect 1: The promise starts and moves to the event loop’s microtask queue.
- Effect 2: The
setTimeout
is placed in the macrotask queue with a 0ms delay. - The
setTimeout
executes before the promise resolves because macrotasks execute after the current JavaScript stack clears, but before pending microtasks.
Example 2: Promise Resolves Before setTimeout
useEffect(() => {
console.log("Effect 3: Fast Promise starts");
(async () => {
await new Promise((resolve) => setTimeout(resolve, 0)); // Resolves quickly
console.log("Effect 3: Fast Promise resolved");
})();
}, []);
useEffect(() => {
console.log("Effect 4: Slow setTimeout scheduled");
setTimeout(() => {
console.log("Effect 4: Slow setTimeout executed");
}, 100); // Delayed timer
}, []);
Effect 3: Fast Promise starts
Effect 4: Slow setTimeout scheduled
Effect 3: Fast Promise resolved
Effect 4: Slow setTimeout executed
Why:
- Promises are part of microtasks and always have higher priority over macrotasks like
setTimeout
. - The promise resolves before the 100ms timer executes.
Example 3: Multiple Effects with Different Timing
useEffect(() => {
console.log("Effect 5: Medium Promise starts");
(async () => {
await new Promise((resolve) => setTimeout(resolve, 500)); // Medium delay
console.log("Effect 5: Medium Promise resolved");
})();
}, []);
useEffect(() => {
console.log("Effect 6: Immediate setTimeout scheduled");
setTimeout(() => {
console.log("Effect 6: Immediate setTimeout executed");
}, 0); // Minimal delay
}, []);
useEffect(() => {
console.log("Effect 7: Long Promise starts");
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000)); // Long delay
console.log("Effect 7: Long Promise resolved");
})();
}, []);
Effect 5: Medium Promise starts
Effect 6: Immediate setTimeout scheduled
Effect 7: Long Promise starts
Effect 6: Immediate setTimeout executed
Effect 5: Medium Promise resolved
Effect 7: Long Promise resolved
Explanation:
- Effects execute in the order they are defined, but their asynchronous behavior governs when they complete.
- The
setTimeout
in Effect 6 executes before the promises resolve due to macrotask prioritization. - Promises resolve based on their individual timing.
Key Takeaways from Multiple useEffect
Hooks:
- Order of Execution: React executes
useEffect
hooks sequentially after rendering, but asynchronous tasks like promises and timers are deferred to the event loop. - Microtasks vs Macrotasks: Promises are microtasks and have higher priority than macrotasks like
setTimeout
. - Empty Dependency Array (
[]
): Ensures the effect runs only once, on the mount. - Timing Dependencies: Faster-resolving promises can interrupt even a setTimeout with 0ms delay.
A Fun Twist: Simultaneous Promises and Timers
What if a promise and setTimeout
are scheduled at the exact same time?
useEffect(() => {
console.log("Effect 8: Quick Promise starts");
(async () => {
await Promise.resolve(); // Instantly resolved
console.log("Effect 8: Quick Promise resolved");
})();
}, []);
useEffect(() => {
console.log("Effect 9: Immediate setTimeout scheduled");
setTimeout(() => {
console.log("Effect 9: Immediate setTimeout executed");
}, 0);
}, []);
Effect 8: Quick Promise starts
Effect 9: Immediate setTimeout scheduled
Effect 8: Quick Promise resolved
Effect 9: Immediate setTimeout executed
Why:
- Promises resolve before
setTimeout
because microtasks are handled before macrotasks in JavaScript's event loop.
Wrapping It Up
React developers often deal with multiple useEffect
hooks. Understanding how promises and timers interact within the event loop helps in writing predictable and efficient code. Always remember:
- Promises resolve sooner than timers due to their higher priority.
- Effects are synchronous unless their code explicitly introduces asynchrony.
Happy coding! 🚀
This article not only teaches React concepts but also introduces core JavaScript mechanics, making it perfect for learners and professionals.