React: Multiple useEffect Hooks with [] Empty Dependency: Promises vs setTimeout

Himanshu Satija
3 min readNov 19, 2024

--

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:

  1. Effect 1: The promise starts and moves to the event loop’s microtask queue.
  2. Effect 2: The setTimeout is placed in the macrotask queue with a 0ms delay.
  3. 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:

  1. Effects execute in the order they are defined, but their asynchronous behavior governs when they complete.
  2. The setTimeout in Effect 6 executes before the promises resolve due to macrotask prioritization.
  3. Promises resolve based on their individual timing.

Key Takeaways from Multiple useEffect Hooks:

  1. Order of Execution: React executes useEffect hooks sequentially after rendering, but asynchronous tasks like promises and timers are deferred to the event loop.
  2. Microtasks vs Macrotasks: Promises are microtasks and have higher priority than macrotasks like setTimeout.
  3. Empty Dependency Array ([]): Ensures the effect runs only once, on the mount.
  4. 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.

--

--

Himanshu Satija
Himanshu Satija

Written by Himanshu Satija

Javascript enthusiast | Frontend Developer at Zoomcar

No responses yet