Hooks let you use state and other React features in function components. They replaced class component lifecycle methods with a cleaner, more composable API.
useState
JSX
const [value, setValue] = useState(initialValue);
// Lazy initialization (expensive computation)
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem('data')) || [];
});
// Multiple state variables
const [name, setName] = useState('');
const [age, setAge] = useState(0);
// OR combine into one object
const [form, setForm] = useState({ name: '', age: 0 });useEffect
Runs side effects after render. Replaces componentDidMount, componentDidUpdate, and componentWillUnmount.
JSX
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
setLoading(false);
}
} catch (err) {
if (!cancelled) setLoading(false);
}
}
fetchUser();
// Cleanup function (like componentWillUnmount)
return () => { cancelled = true; };
}, [userId]); // Re-run when userId changes
if (loading) return <p>Loading...</p>;
if (!user) return <p>User not found</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}useEffect Patterns
JSX
// Run once on mount (empty dependency array)
useEffect(() => {
console.log('Component mounted');
}, []);
// Run on every render (no dependency array)
useEffect(() => {
console.log('Rendered');
});
// Run when specific values change
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // Only re-runs when count changes
// Cleanup on unmount or before re-run
useEffect(() => {
const subscription = someService.subscribe(handleEvent);
return () => subscription.unsubscribe();
}, []);useRef
Access DOM elements or persist values without causing re-reounds.
JSX
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</>
);
}
// Persist value across renders
function Stopwatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>{time}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}useContext
Access context without prop drilling.
JSX
// Create context
const ThemeContext = createContext('light');
// Provider
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Dashboard />
</ThemeContext.Provider>
);
}
// Consumer
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
className={theme}
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
Toggle Theme
</button>
);
}useMemo and useCallback
JSX
// useMemo — memoize expensive calculations
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]); // Only re-calculate when items change
// useCallback — memoize functions (prevents child re-renders)
const handleClick = useCallback((id) => {
setSelectedId(id);
}, []); // Function identity stays stable
// Pass to child — won't cause unnecessary re-renders
<DataTable onSelect={handleClick} />Custom Hooks
Extract reusable logic into custom hooks:
JSX
// useFetch.js
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => { setData(data); setLoading(false); })
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return users.map(u => <UserCard key={u.id} user={u} />);
}
// useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// useDebounce
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}💡 Tip: Follow the Rules of Hooks — only call hooks at the top level (not in loops, conditions, or nested functions) and only call them from React functions.
Next: APIs and Data Fetching