Mastering the react useeffect hook is a fundamental milestone for any modern frontend developer. In modern React development, functional components focus heavily on rendering user interfaces based on state changes. However, web applications frequently need to step outside this pure rendering loop to synchronize data with external systems—such as fetching API parameters, managing browser listeners, or triggering local timers.
These asynchronous operations are known as side effects, and managing them improperly will instantly degrade application execution.
To understand how to prepare your underlying data logic prior to rendering your UI, read our comprehensive JavaScript Array Methods Cheatsheet.
The Blueprint of the React useEffect Hook
The hook takes two arguments: a callback function containing your side effect logic, and an optional dependency array that controls exactly when that callback runs.
JavaScript
import { useEffect } from 'react'
useEffect(() => {
// Your side effect code runs here
}, [dependencies]);
1. Controlling the Dependency Array Behavior
The dependency array is the throttle of your execution layer. Omitting or altering it completely changes how React handles your component lifecycle:

Case A: No Dependency Array (Runs on EVERY Render)
If you do not pass an array at all, the function fires after the initial mount and after every single state or prop update. This is a dangerous pattern that can quickly freeze browsers.
JavaScript
useEffect(() => {
console.log('I run on every single render pass!');
});
Case B: Empty Dependency Array (Runs ONCE on Mount)
An empty brackets array [] explicitly instructs React to execute the side effect function exactly once when the component initially mounts onto the DOM interface.
JavaScript
useEffect(() => {
console.log('Component successfully mounted.');
}, []);
Case C: Specific Dependencies (Runs on Target Matches)
Passing specific variable nodes inside the array tracks them actively. The callback fires on mount, and then fires again only if those specific values mutate.
JavaScript
useEffect(() => {
console.log(`The active user ID changed to: ${userId}`);
}, [userId]);
2. The Crucial Component Cleanup Function
Many side effects consume active browser memory infrastructure—like setting up a setInterval or appending a global window listener. If you leave these running when a component unmounts, you create severe memory leaks.
To fix this, return a clean function directly out of your callback. React executes this cleanup routine right before the component unmounts or before re-running the side effect layer.
JavaScript
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
// Return the cleanup logic
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
For alternative baseline reference guidelines concerning handling runtime states cleanly without side-effect cascades, check out the official documentation on the React.dev Hooks Reference Hub.
This happens because your application is running in React’s “Strict Mode” during local development (<React.StrictMode>). React intentionally mounts, unmounts, and remounts your component on boot to help you detect hidden memory leaks and verify that your cleanup routines work correctly before pushing to production.
An infinite loop occurs when your effect updates a piece of state, and that exact same state is listed as a dependency inside your array tracker. To fix it, either remove the updating state from the brackets array, or pass a functional state updater callback (like setCount(prev => prev + 1)) which reads the current state value without tracking it as a core dependency.
No, you cannot pass an async function directly as the first argument to useEffect because async functions implicitly return a Promise, whereas React demands that the hook return either nothing or a synchronous cleanup function. To run asynchronous code safely, declare your async function inside the effect callback wrapper and invoke it immediately below.