Understanding React Hooks: A Complete Guide
Introduction to Hooks
Hooks were introduced in React 16.8 to allow functional components to use state and lifecycle features. They provide a more elegant and reusable way to share logic between components compared to class components.
useState - Managing State
useState is the most fundamental hook. It allows you to add state to functional components. Each call to useState creates an independent piece of state.
import { useState } from 'react'; function Counter() { // Declare state variable with initial value const [count, setCount] = useState(0); // Functional update for state based on previous value const increment = () => setCount(prev => prev + 1); // Object state example const [user, setUser] = useState({ name: '', email: '' }); const updateName = (name: string) => { setUser(prev => ({ ...prev, name })); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> );}useEffect - Side Effects
useEffect handles side effects like data fetching, subscriptions, and DOM manipulation. It runs after render and can optionally clean up before the next effect or unmount.
import { useState, useEffect } from 'react'; function UserProfile({ userId }: { userId: string }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // Effect function async function fetchUser() { setLoading(true); const response = await fetch(`/api/users/${userId}`); const data = await response.json(); setUser(data); setLoading(false); } fetchUser(); // Cleanup function (optional) return () => { // Cancel pending requests, unsubscribe, etc. }; }, [userId]); // Dependency array - effect runs when userId changes if (loading) return <div>Loading...</div>; return <div>{user?.name}</div>;}useMemo & useCallback - Optimization
These hooks help optimize performance by memoizing values and functions. Use them when you have expensive computations or when passing callbacks to child components.
import { useMemo, useCallback } from 'react'; function ExpensiveList({ items, filter }) { // Memoize expensive computation const filteredItems = useMemo(() => { console.log('Filtering items...'); return items.filter(item => item.includes(filter)); }, [items, filter]); // Only recompute when items or filter changes // Memoize callback function const handleClick = useCallback((id: string) => { console.log('Clicked:', id); }, []); // Empty deps = function never changes return ( <ul> {filteredItems.map(item => ( <ListItem key={item} onClick={handleClick} /> ))} </ul> );}useRef - Mutable References
useRef creates a mutable reference that persists across renders without causing re-renders when changed. Perfect for DOM references and storing previous values.
import { useRef, useEffect } from 'react'; function TextInput() { const inputRef = useRef<HTMLInputElement>(null); const renderCount = useRef(0); useEffect(() => { // Focus input on mount inputRef.current?.focus(); renderCount.current += 1; }); return ( <div> <input ref={inputRef} type="text" /> <p>Renders: {renderCount.current}</p> </div> );}Custom Hooks - Reusable Logic
Custom hooks let you extract component logic into reusable functions. They must start with 'use' and can call other hooks.
// Custom hook for fetching datafunction useFetch<T>(url: string) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { const controller = new AbortController(); async function fetchData() { try { setLoading(true); const res = await fetch(url, { signal: controller.signal }); const json = await res.json(); setData(json); } catch (e) { if (e.name !== 'AbortError') setError(e); } finally { setLoading(false); } } fetchData(); return () => controller.abort(); }, [url]); return { data, loading, error };} // Usagefunction Profile() { const { data, loading } = useFetch('/api/profile'); if (loading) return <div>Loading...</div>; return <div>{data?.name}</div>;}Rules of Hooks
- Only call hooks at the top level (not inside loops, conditions, or nested functions)
- Only call hooks from React functions (components or custom hooks)
- Custom hooks must start with 'use'
- Hooks are called in the same order every render
Conclusion
React Hooks have transformed how we write React applications. By understanding each hook's purpose and following best practices, you can write cleaner, more maintainable code. Start with useState and useEffect, then gradually incorporate optimization hooks as needed.