useEffect - Deep Dive Into It

What is a side effect?
A side effect is an operation with an observable impact outside the primary render result. In React, effects are useful when something cannot happen during rendering or when a side effect should happen because a piece of UI is now visible.
Posting a comment, deleting a row from a table, connecting to a chatroom server, playing media, or fetching related data can all be side effects. The important part is that rendering itself should stay pure.
Synchronizing with effects
Effects run after the commit phase, after React updates the screen. Every time state or props change, React renders, commits the result to the UI, and only then runs the code inside useEffect.
This delay is exactly what we need when a side effect depends on DOM nodes. For example, a video element does not exist during the first render, so trying to call play or pause during render will crash.
import { useEffect, useRef } from "react"; function VideoPlayer({ src, isPlaying }) { const ref = useRef<HTMLVideoElement | null>(null); useEffect(() => { if (!ref.current) return; if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />;}How to control effects
By default, an effect runs after every render. If that effect updates state every time, you can create an infinite loop: render, run effect, update state, render again, and repeat.
The dependency array is how you tell React when the effect should re-synchronize. No array means every render. An empty array means mount only. A list of values means run when one of those values changes.
useEffect(() => { // Runs after every render.}); useEffect(() => { // Runs once when the component appears.}, []); useEffect(() => { // Runs when count or shouldShow changes.}, [count, shouldShow]);Clean it up
Every time we create an effect, we should ask what needs to stop when the input changes or the component disappears. Fetches may need cancellation, subscriptions may need disconnection, and timers may need clearing.
Think of cleanup as stopping synchronization with the old value before React starts synchronizing with the new one.
export default function FaqsList({ category }) { useEffect(() => { const controller = new AbortController(); fetchFaqData(category, { signal: controller.signal }); return () => { controller.abort(); }; }, [category]); return <>{/* render faqs */}</>;}The lifecycle of an effect
A component mounts, updates, and unmounts. An effect has a different lifecycle: it starts synchronizing with something, stops synchronizing, and then may start again with new values.
If a user switches an FAQ category from general to sport, React first runs the cleanup for general, then runs the effect for sport. That keeps stale work from competing with the latest user intent.
How React synchronizes the effect
React knows when to re-synchronize because you provide the dependencies. If any dependency is different from the value in the previous render, React runs the cleanup and then runs the effect again.
React compares dependencies with Object.is. That means objects and functions created during render can cause effects to run more often than expected because their references are recreated each render.
Summary
Effects should usually synchronize your component with an external system. If there is no external system, the effect might be unnecessary.
Effects only run on the client. You cannot choose dependencies casually: every reactive value used in the effect belongs in the dependency list.
Avoid relying on freshly created objects or functions as dependencies unless you understand the reference changes they create.
Comments
()The effect lifecycle explanation is the part I wish I had earlier.