What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →An effect that fires forever, runs twice, or reads stale state. Here is what is actually happening with React's useEffect dependencies, and the patterns that make effects predictable.
useEffect is probably the most misunderstood hook in React. It looks simple, then one day your network tab fills with the same request a few hundred times. Let's break down why effects loop, why they run twice, and when you shouldn't be using an effect at all.
An effect runs after render, and then again whenever any value in its dependency array changes between renders. React compares dependencies with Object.is, which means for objects, arrays and functions it checks identity, not contents.
Keep that in mind. It explains nearly every bug below.
const [count, setCount] = useState(0)
useEffect(() => {
setCount(count + 1)
}, [count])
Render → effect sets count → re-render → count changed → effect runs → ... forever.
Fix: ask whether this needs to be an effect at all. If it truly does, use the functional updater and remove the dependency:
useEffect(() => {
setCount(c => c + 1)
}, []) // runs once on mount
const filters = { status: 'active' }
useEffect(() => {
fetchUsers(filters)
}, [filters])
filters is a new object on every render. Same contents, different identity, so the effect runs after every render. If fetchUsers sets state, you've built a loop.
Fixes, in order of preference:
[filters.status].const filters = useMemo(() => ({ status }), [status]).const load = () => fetchUsers(page)
useEffect(() => {
load()
}, [load]) // new function every render
Same identity problem. Move the function inside the effect, or wrap it in useCallback with the right dependencies. Newer React tooling like the React Compiler can memoize a lot of this automatically, but understanding why still matters when something loops.
Leaving values out of the array to "stop the loop" creates stale closures: the effect keeps using old props or state.
useEffect(() => {
const id = setInterval(() => console.log(count), 1000)
return () => clearInterval(id)
}, []) // always logs the initial count
Keep the react-hooks/exhaustive-deps lint rule on. When it complains, fix the cause (identity, structure) rather than silencing it.
In development with <StrictMode>, React intentionally mounts, unmounts and remounts components to surface effects that don't clean up properly. That's why you see two requests locally. It does not happen in production builds.
The right response isn't removing Strict Mode. It's writing effects that survive being run twice, which mostly means adding cleanup.
useEffect(() => {
const controller = new AbortController()
fetch(`/api/users?page=${page}`, { signal: controller.signal })
.then(res => res.json())
.then(setUsers)
.catch(err => {
if (err.name !== 'AbortError') setError(err)
})
return () => controller.abort()
}, [page])
The cleanup cancels the old request when page changes or the component unmounts. That fixes the Strict Mode double fetch and the race condition where a slow earlier response overwrites a newer one.
For anything beyond simple cases, a data-fetching library (TanStack Query, SWR) or your framework's server-side data loading handles caching, deduplication and retries for you. In Next.js, fetching in Server Components often removes the effect entirely.
Many effects shouldn't exist. Common cases:
fullName into state with an effect. Compute it during render: const fullName = first + ' ' + last.key instead: <Profile key={userId} />.useMemo, not an effect plus state.Effects are for synchronising with things outside React: network requests, subscriptions, timers, browser APIs, third-party widgets. If no external system is involved, there's probably a simpler way.
If your effect is async and behaving strangely, these async/await mistakes are worth a look too.
Pass an empty dependency array: useEffect(fn, []). In development Strict Mode it will still run twice on mount, by design, so make sure it has proper cleanup.
Usually because it updates state that's in its own dependency array, or because a dependency is an object, array or function recreated on every render.
Not directly, because the callback must return nothing or a cleanup function. Define an async function inside the effect and call it, or use promise chains as shown above.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →