Home Quizzes Leaderboard Competitions Learn Hire Us
About Contact
Log In Sign Up
Learn Next.js Hooks --- useEffect, useContext & Custom Hooks

Hooks --- useEffect, useContext & Custom Hooks

⏱ 18 min read read
useEffect --- Side Effects in Client Components:

useEffect(() => {

// runs after render

return () => { /* cleanup */ };

}, [dependencies]);

useEffect(() => { ... }, []); // run once on mount

useEffect(() => { ... }); // run after every render

useEffect(() => { ... }, [id]); // run when 'id' changes

In Next.js, prefer Server Components for data fetching over
useEffect.

useEffect runs ONLY in the browser --- avoid for initial data loads.

Common legitimate uses: subscriptions, timers, window event
listeners,

syncing with external non-React systems, localStorage.

Always return a cleanup function for subscriptions and timers.

useContext --- Share State Without Prop Drilling:

// 1. Create context

const ThemeContext = createContext<'light' |
'dark'>('light');

// 2. Provide it high up in the tree

function App() {

const [theme, setTheme] = useState<'light' |
'dark'>('light');

return (

<ThemeContext.Provider value={theme}>

<Page />

</ThemeContext.Provider>

);

}

// 3. Consume anywhere in the tree

function Button() {

const theme = useContext(ThemeContext);

return <button className={theme}>Click</button>;

}

Custom Hooks --- Reusable Logic:

A custom hook is just a function that starts with 'use' and calls
other hooks. Extract repeated patterns into custom hooks.

function useLocalStorage<T>(key: string, initialValue: T) {

const [value, setValue] = useState<T>(() => {

if (typeof window === 'undefined') return initialValue;

const stored = localStorage.getItem(key);

return stored ? JSON.parse(stored) : initialValue;

});

const set = (v: T) => {

setValue(v);

localStorage.setItem(key, JSON.stringify(v));

};

return [value, set] as const;

}

// Usage

const [name, setName] = useLocalStorage('username', '');
Code Example
'use client';

import { useState, useEffect, useContext, createContext, useCallback
} from 'react';

// Custom hook --- fetch with loading/error state

function useFetch<T>(url: string) {

const [data, setData] = useState<T | null>(null);

const [loading, setLoading] = useState(true);

const [error, setError] = useState<string | null>(null);

useEffect(() => {

setLoading(true);

fetch(url)

.then(r => { if (!r.ok) throw new Error('Fetch failed'); return
r.json(); })

.then(d => { setData(d); setError(null); })

.catch(e => setError(e.message))

.finally(() => setLoading(false));

}, [url]);

return { data, loading, error };

}

// Custom hook --- debounce

function useDebounce<T>(value: T, delay = 300): T {

const [debounced, setDebounced] = useState(value);

useEffect(() => {

const t = setTimeout(() => setDebounced(value), delay);

return () => clearTimeout(t);

}, [value, delay]);

return debounced;

}

// Custom hook --- window size

function useWindowSize() {

const [size, setSize] = useState({ width: 0, height: 0 });

useEffect(() => {

const update = () => setSize({ width: window.innerWidth, height:
window.innerHeight });

update();

window.addEventListener('resize', update);

return () => window.removeEventListener('resize', update);

}, []);

return size;

}

// Usage in a component

export default function SearchPage() {

const [query, setQuery] = useState('');

const debouncedQuery = useDebounce(query, 400);

const { width } = useWindowSize();

const { data, loading, error } = useFetch<any[]>(

`https://jsonplaceholder.typicode.com/posts?q=${debouncedQuery}`

);

return (

<div>

<p>Window: {width}px</p>

<input value={query} onChange={e => setQuery(e.target.value)}
placeholder="Search..." />

{loading && <p>Loading...</p>}

{error && <p style={{ color: 'red' }}>{error}</p>}

{data?.slice(0, 5).map(p => <p key={p.id}>{p.title}</p>)}

</div>

);

}
← State & Client Interactivity Forms & Server Actions →

Log in to track your progress and earn badges as you complete lessons.

Log In to Track Progress