useEffect & Side Effects
What is a side effect?
A component's main job is to return JSX. Anything else that reaches outside — starting a timer, reading localStorage, calling an API, changing document.title — is a side effect. useEffect runs such code after React has updated the screen.
useEffect syntax
'use client'
import { useEffect, useState } from 'react'
useEffect(() => {
// 1. code that touches the outside world
return () => {
// 2. optional cleanup
}
}, [/* 3. dependencies */])The dependency array decides when it runs
| You write | Effect runs | Use for |
|---|---|---|
useEffect(fn, []) | Once, after first render | Start a timer, load from storage |
useEffect(fn, [q]) | First render + whenever q changes | Search when text changes |
useEffect(fn) | After every render | Rarely needed — be careful |
Example: a live clock with cleanup
'use client'
import { useEffect, useState } from 'react'
export default function Clock() {
const [time, setTime] = useState(null) // null on the server, real time in the browser
useEffect(() => {
setTime(new Date())
const id = setInterval(() => setTime(new Date()), 1000) // start
return () => clearInterval(id) // cleanup: stop
}, []) // [] = run once after the first render
return <p>⏰ {time ? time.toLocaleTimeString('en-IN') : '--:--:--'}</p>
}Without clearInterval, leaving the page would keep the timer running in the background — a memory leak. The cleanup function prevents that.
null and set the time inside the effect. The server and browser clocks differ, and showing the time directly during render would cause a hydration mismatch warning.Example: save a note in localStorage
'use client'
import { useEffect, useState } from 'react'
export default function Notes() {
const [text, setText] = useState('')
// Load once from the browser's storage
useEffect(() => {
setText(localStorage.getItem('note') || '')
}, [])
// Save every time `text` changes
useEffect(() => {
localStorage.setItem('note', text)
}, [text])
return <textarea value={text} onChange={(e) => setText(e.target.value)} rows={4} />
}localStorage exists only in the browser, so it must be used inside useEffect (never at the top of a component, which also runs on the server).
Example: live search with fetch
'use client'
import { useEffect, useState } from 'react'
export default function LiveSearch() {
const [q, setQ] = useState('')
const [results, setResults] = useState([])
useEffect(() => {
if (!q) return setResults([])
const controller = new AbortController()
const timer = setTimeout(async () => { // wait 300 ms after typing stops
const res = await fetch(`/api/courses?q=${encodeURIComponent(q)}`, { signal: controller.signal })
const data = await res.json()
setResults(data.courses)
}, 300)
return () => { clearTimeout(timer); controller.abort() } // cancel old request
}, [q])
return (
<div>
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search courses" />
<ul>{results.map((c) => <li key={c.slug}>{c.title} – ₹{c.fee}</li>)}</ul>
</div>
)
}In Next.js, prefer the server for data
For data that is part of the page (course lists, blog posts), a Server Component is simpler and better for SEO — the HTML already contains the data and Google can read it:
// app/courses/page.jsx — Server Component: no useEffect, no loading spinner needed
import { searchCourses } from '@/lib/courses'
export default async function CoursesPage({ searchParams }) {
const { q = '' } = await searchParams
const list = searchCourses(q) // runs on the server
return <ul>{list.map((c) => <li key={c.slug}>{c.title}</li>)}</ul>
}useEffect + fetch (client)
- Page first shows empty / spinner
- Extra request from the browser
- Search engines may miss content
- Good for live, user-driven updates
Server Component (await)
- HTML arrives with data
- No loading flicker
- Best for SEO
- Good for page content
Common mistakes
- Setting state in an effect with no dependency array → infinite render loop.
- Missing dependencies → effect uses an old value. Include every variable from the component that the effect uses.
- Making the effect function itself
async→ not allowed. Create an async function inside and call it. - Using
windoworlocalStorageoutsideuseEffect→ "window is not defined" on the server.
Summary
useEffectruns side effects after render.- The dependency array controls when it runs; return a cleanup function to undo work.
- Use it for timers, storage, subscriptions and live search.
- For page data in Next.js, fetch in Server Components instead.
Make a component that changes document.title to "(3) New messages" based on a count in state, and resets the title when the component is removed.
