useState
useState is the most fundamental hook. It adds state to functional components and returns the current value and a setter function.
'use client';
import { useState } from 'react';
// Simple state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(prev => prev - 1)}>-</button>
</div>
);
}
// Object state
interface FormData {
name: string;
email: string;
role: string;
}
function UserForm() {
const [form, setForm] = useState<FormData>({
name: '',
email: '',
role: 'developer',
});
const handleChange = (field: keyof FormData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setForm(prev => ({ ...prev, [field]: e.target.value }));
};
return (
<form>
<input value={form.name} onChange={handleChange('name')} />
<input value={form.email} onChange={handleChange('email')} />
</form>
);
}
useEffect
useEffect handles side effects — data fetching, subscriptions, DOM manipulation, and timers. The cleanup function prevents memory leaks.
'use client';
import { useState, useEffect } from 'react';
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false; // Prevent state update on unmount
async function loadUser() {
try {
setLoading(true);
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) {
setUser(data);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError('Failed to load user');
}
} finally {
if (!cancelled) setLoading(false);
}
}
loadUser();
// Cleanup: cancel if userId changes or component unmounts
return () => { cancelled = true; };
}, [userId]); // Re-run when userId changes
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return null;
return <div>{user.name}</div>;
}
useCallback & useMemo
These hooks optimize performance by memoizing functions and computed values, preventing unnecessary re-renders.
'use client';
import { useState, useCallback, useMemo } from 'react';
function ExpensiveComponent({ items, onSelect }: {
items: string[];
onSelect: (item: string) => void;
}) {
console.log('ExpensiveComponent rendered');
return (
<ul>
{items.map(item => (
<li key={item} onClick={() => onSelect(item)}>{item}</li>
))}
</ul>
);
}
// Wrap with React.memo to prevent re-render unless props change
const MemoizedComponent = React.memo(ExpensiveComponent);
function Parent() {
const [count, setCount] = useState(0);
const [selected, setSelected] = useState('');
const items = ['Apple', 'Banana', 'Cherry'];
// useCallback: memoize function reference
// Without this, new function ref on every render = MemoizedComponent re-renders
const handleSelect = useCallback((item: string) => {
setSelected(item);
}, []); // No deps, same reference always
// useMemo: memoize expensive computation
const expensiveResult = useMemo(() => {
console.log('Computing...');
return items.filter(item => item.toLowerCase().includes('a'));
}, [items]); // Only recompute when items changes
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<p>Selected: {selected}</p>
<MemoizedComponent items={expensiveResult} onSelect={handleSelect} />
</div>
);
}
Don't over-optimize. Only use useCallback and useMemo when you have a measured performance problem. Premature optimization adds complexity.
Custom Hooks
Custom hooks let you extract and reuse stateful logic across components. They always start with "use" and can call other hooks.
import { useState, useEffect } from 'react';
// Custom hook: persists state in localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function
? value(storedValue)
: value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'dark');
return (
<button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
Current theme: {theme}
</button>
);
}