Chapter 12: React Hooks Deep Dive
These are the most powerful advanced hooks that every serious React developer uses daily. We’ll go through each one slowly, clearly, with real-world examples you can copy-paste right now — like I’m sitting next to you in Mumbai explaining it live on the screen.
Let’s dive in! 🚀
1. useRef – For Mutable Values & DOM Access
useRef creates a mutable reference that persists across renders but does NOT cause re-renders when it changes.
Two main uses:
- Accessing DOM elements (focus input, scroll, measure size…)
- Storing mutable values that survive re-renders (previous state, timers, intervals…)
Syntax:
|
0 1 2 3 4 5 6 |
const ref = useRef(initialValue); |
Example 1: Focusing an Input on Button Click
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import { useRef } from 'react'; function FocusInput() { const inputRef = useRef<HTMLInputElement>(null); const focusInput = () => { if (inputRef.current) { inputRef.current.focus(); // ← direct DOM access! } }; return ( <div style={{ padding: '40px', textAlign: 'center' }}> <h2>Click to focus the input!</h2> <input ref={inputRef} // ← attach ref to DOM element type="text" placeholder="Type here..." style={{ padding: '12px', fontSize: '18px', width: '300px' }} /> <button onClick={focusInput} style={{ marginLeft: '20px', padding: '12px 24px', background: '#646cff', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }} > Focus Input </button> </div> ); } |
Example 2: Storing Previous State (Common Pattern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { useState, useEffect, useRef } from 'react'; function PreviousValueDemo() { const [count, setCount] = useState(0); const prevCountRef = useRef<number>(0); useEffect(() => { prevCountRef.current = count; // Update ref after render }, [count]); const prevCount = prevCountRef.current; return ( <div style={{ padding: '40px', textAlign: 'center' }}> <h2>Current: {count}</h2> <h2>Previous: {prevCount}</h2> <button onClick={() => setCount(c => c + 1)} style={{ padding: '12px 24px', background: '#4ecdc4', color: 'white', border: 'none', borderRadius: '8px' }} > +1 </button> </div> ); } |
Key takeaway: useRef value doesn’t trigger re-render when changed — perfect for values you want to remember but not display.
2. useReducer – For Complex State Logic
useReducer is like useState on steroids — great when you have complex state updates or multiple related values.
Syntax:
|
0 1 2 3 4 5 6 |
const [state, dispatch] = useReducer(reducer, initialState); |
- reducer: pure function (state, action) => newState
- dispatch: function to send actions
Example: Todo App with useReducer
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
import { useReducer, useState } from 'react'; interface Todo { id: number; text: string; completed: boolean; } type Action = | { type: 'ADD'; payload: string } | { type: 'TOGGLE'; payload: number } | { type: 'DELETE'; payload: number }; const initialTodos: Todo[] = []; function todoReducer(todos: Todo[], action: Action): Todo[] { switch (action.type) { case 'ADD': return [...todos, { id: Date.now(), text: action.payload, completed: false }]; case 'TOGGLE': return todos.map(todo => todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo ); case 'DELETE': return todos.filter(todo => todo.id !== action.payload); default: return todos; } } function TodoApp() { const [todos, dispatch] = useReducer(todoReducer, initialTodos); const [input, setInput] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!input.trim()) return; dispatch({ type: 'ADD', payload: input }); setInput(''); }; return ( <div style={{ padding: '40px', maxWidth: '600px', margin: '0 auto' }}> <h2 style={{ color: '#646cff' }}>Todo App with useReducer</h2> <form onSubmit={handleSubmit} style={{ marginBottom: '30px', display: 'flex', gap: '12px' }}> <input value={input} onChange={e => setInput(e.target.value)} placeholder="Add new todo..." style={{ flex: 1, padding: '12px', borderRadius: '8px', border: '1px solid #ddd' }} /> <button type="submit" style={{ padding: '12px 24px', background: '#646cff', color: 'white', border: 'none', borderRadius: '8px' }} > Add </button> </form> <ul style={{ listStyle: 'none', padding: 0 }}> {todos.map(todo => ( <li key={todo.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '12px', margin: '8px 0', background: todo.completed ? '#e0ffe0' : '#f0f4ff', borderRadius: '8px' }} > <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} onClick={() => dispatch({ type: 'TOGGLE', payload: todo.id })} > {todo.text} </span> <button onClick={() => dispatch({ type: 'DELETE', payload: todo.id })} style={{ background: '#ff6b6b', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '6px' }} > Delete </button> </li> ))} </ul> </div> ); } |
When to choose useReducer over useState:
- State is an object/array with multiple fields
- Updates depend on previous state in complex ways
- You want action-based logic (like Redux)
3. useContext – Sharing State Without Prop Drilling
useContext lets you share state deeply in the component tree without passing props through every level.
Steps:
- Create Context
- Provide value at top level
- Consume with useContext
Example: Theme Switcher
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
import { createContext, useContext, useState, ReactNode } from 'react'; // 1. Create Context interface ThemeContextType { theme: 'light' | 'dark'; toggleTheme: () => void; } const ThemeContext = createContext<ThemeContextType | undefined>(undefined); // 2. Provider Component function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<'light' | 'dark'>('light'); const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } // 3. Custom hook (best practice) function useTheme() { const context = useContext(ThemeContext); if (!context) throw new Error('useTheme must be used within ThemeProvider'); return context; } // Child component deep in the tree function ThemedButton() { const { theme, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme} style={{ padding: '12px 24px', background: theme === 'dark' ? '#333' : '#646cff', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }} > Toggle Theme (Current: {theme}) </button> ); } // App function App() { return ( <ThemeProvider> <div style={{ padding: '40px', minHeight: '100vh', background: useTheme().theme === 'dark' ? '#222' : '#f8f9ff', color: useTheme().theme === 'dark' ? 'white' : 'black', transition: 'all 0.3s' }}> <h1>useContext Theme Example</h1> <ThemedButton /> {/* Even 10 levels deep → still access theme! */} </div> </ThemeProvider> ); } |
4. useMemo & useCallback – Performance Optimization
Both memoize values/functions so they don’t get recreated on every render.
| Hook | What it memoizes | When to use |
|---|---|---|
| useMemo | Value (calculation) | Expensive calculations (filtering big array, complex math) |
| useCallback | Function | When passing callbacks to child components (prevents unnecessary re-renders) |
Example: useMemo for Expensive List Filtering
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { useState, useMemo } from 'react'; function ExpensiveFilter() { const [numbers] = useState(() => Array.from({ length: 100000 }, (_, i) => i)); const [filter, setFilter] = useState(50000); const filteredNumbers = useMemo(() => { console.log('Filtering...'); // This should run only when filter changes return numbers.filter(n => n > filter); }, [numbers, filter]); return ( <div style={{ padding: '40px' }}> <h2>Filtered Numbers (> {filter})</h2> <input type="range" min="0" max="100000" value={filter} onChange={e => setFilter(Number(e.target.value))} style={{ width: '100%' }} /> <p>Count: {filteredNumbers.length}</p> </div> ); } |
Example: useCallback to Prevent Child Re-renders
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { useState, useCallback } from 'react'; function Child({ onClick }: { onClick: () => void }) { console.log('Child rendered'); return <button onClick={onClick}>Click me</button>; } function Parent() { const [count, setCount] = useState(0); // Without useCallback → new function every render → Child re-renders every time! const handleClick = useCallback(() => { setCount(c => c + 1); }, []); // dependencies empty → stable function return ( <div> <h2>Count: {count}</h2> <Child onClick={handleClick} /> </div> ); } |
5. Custom Hooks – Reusing Logic
Custom hooks are functions that start with use and can call other hooks.
Example: useLocalStorage (Super Useful!)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import { useState, useEffect } from 'react'; function useLocalStorage<T>(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { return initialValue; } }); useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(storedValue)); } catch (error) { console.error(error); } }, [key, storedValue]); return [storedValue, setStoredValue] as const; } // Usage function ThemeSwitcher() { const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light'); return ( <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} style={{ padding: '12px 24px', background: theme === 'dark' ? '#333' : '#646cff', color: 'white' }} > Toggle Theme (Saved in localStorage!) </button> ); } |
Summary – Chapter 12 Key Takeaways
- useRef → DOM access + mutable values that don’t cause re-renders
- useReducer → complex state logic, action-based updates
- useContext → share state deeply without prop drilling
- useMemo → memoize expensive calculations
- useCallback → memoize functions (especially callbacks to children)
- Custom hooks → extract and reuse stateful logic
Mini Homework
- Create useWindowSize custom hook that returns { width, height } and updates on resize
- Use it in a component to show different content on mobile vs desktop
