Back to Blog
ReactHooksuseStateuseEffect

React Hooks: A Deep Dive

Master React hooks. From useState to custom hooks to advanced patterns and pitfalls.

B
Bootspring Team
Engineering
July 15, 2022
6 min read

Hooks are the foundation of modern React. Here's how to use them effectively and avoid common pitfalls.

useState#

1// Basic usage 2const [count, setCount] = useState(0); 3 4// Lazy initialization (expensive computation) 5const [data, setData] = useState(() => { 6 return computeExpensiveInitialValue(); 7}); 8 9// Functional updates (when new state depends on previous) 10setCount(prev => prev + 1); 11 12// Object state 13const [user, setUser] = useState({ name: '', email: '' }); 14 15// Update single field 16setUser(prev => ({ ...prev, name: 'John' })); 17 18// Common mistake: stale closure 19function Counter() { 20 const [count, setCount] = useState(0); 21 22 useEffect(() => { 23 const interval = setInterval(() => { 24 // ❌ Always logs 0 (stale closure) 25 console.log(count); 26 27 // ❌ Always sets to 1 28 setCount(count + 1); 29 30 // ✓ Uses latest value 31 setCount(prev => prev + 1); 32 }, 1000); 33 34 return () => clearInterval(interval); 35 }, []); // Empty deps means closure captures initial values 36 37 return <div>{count}</div>; 38}

useEffect#

1// Run on every render 2useEffect(() => { 3 console.log('Rendered'); 4}); 5 6// Run once on mount 7useEffect(() => { 8 fetchData(); 9}, []); 10 11// Run when dependencies change 12useEffect(() => { 13 fetchUser(userId); 14}, [userId]); 15 16// Cleanup function 17useEffect(() => { 18 const subscription = subscribe(userId); 19 20 return () => { 21 subscription.unsubscribe(); 22 }; 23}, [userId]); 24 25// Async in useEffect 26useEffect(() => { 27 // ❌ Can't make useEffect async directly 28 // async () => { ... } 29 30 // ✓ Define async function inside 31 async function fetchData() { 32 const result = await api.getData(); 33 setData(result); 34 } 35 36 fetchData(); 37}, []); 38 39// With abort controller 40useEffect(() => { 41 const controller = new AbortController(); 42 43 async function fetchData() { 44 try { 45 const response = await fetch(url, { 46 signal: controller.signal, 47 }); 48 const data = await response.json(); 49 setData(data); 50 } catch (error) { 51 if (error.name !== 'AbortError') { 52 setError(error); 53 } 54 } 55 } 56 57 fetchData(); 58 59 return () => controller.abort(); 60}, [url]);

useMemo and useCallback#

1// useMemo: Memoize computed values 2const expensiveValue = useMemo(() => { 3 return computeExpensiveValue(a, b); 4}, [a, b]); 5 6// useCallback: Memoize functions 7const handleClick = useCallback(() => { 8 doSomething(a, b); 9}, [a, b]); 10 11// When to use useMemo 12function SearchResults({ query, items }: Props) { 13 // ✓ Expensive filtering 14 const filteredItems = useMemo(() => { 15 return items.filter(item => 16 item.name.toLowerCase().includes(query.toLowerCase()) 17 ); 18 }, [query, items]); 19 20 // ❌ Don't memoize cheap operations 21 const upperQuery = query.toUpperCase(); // Just compute it 22 23 return <List items={filteredItems} />; 24} 25 26// When to use useCallback 27function Parent() { 28 const [count, setCount] = useState(0); 29 30 // Without useCallback, Child re-renders on every Parent render 31 const handleClick = useCallback(() => { 32 setCount(c => c + 1); 33 }, []); 34 35 return <MemoizedChild onClick={handleClick} />; 36} 37 38const MemoizedChild = React.memo(function Child({ onClick }) { 39 return <button onClick={onClick}>Click</button>; 40});

useRef#

1// DOM reference 2function TextInput() { 3 const inputRef = useRef<HTMLInputElement>(null); 4 5 const focusInput = () => { 6 inputRef.current?.focus(); 7 }; 8 9 return <input ref={inputRef} />; 10} 11 12// Mutable value that persists across renders 13function Timer() { 14 const [count, setCount] = useState(0); 15 const intervalRef = useRef<number | null>(null); 16 17 useEffect(() => { 18 intervalRef.current = setInterval(() => { 19 setCount(c => c + 1); 20 }, 1000); 21 22 return () => { 23 if (intervalRef.current) { 24 clearInterval(intervalRef.current); 25 } 26 }; 27 }, []); 28 29 const stop = () => { 30 if (intervalRef.current) { 31 clearInterval(intervalRef.current); 32 } 33 }; 34 35 return ( 36 <div> 37 {count} 38 <button onClick={stop}>Stop</button> 39 </div> 40 ); 41} 42 43// Previous value 44function usePrevious<T>(value: T): T | undefined { 45 const ref = useRef<T>(); 46 47 useEffect(() => { 48 ref.current = value; 49 }, [value]); 50 51 return ref.current; 52} 53 54function Counter() { 55 const [count, setCount] = useState(0); 56 const prevCount = usePrevious(count); 57 58 return ( 59 <div> 60 Now: {count}, Before: {prevCount} 61 </div> 62 ); 63}

useReducer#

1interface State { 2 count: number; 3 error: string | null; 4} 5 6type Action = 7 | { type: 'increment' } 8 | { type: 'decrement' } 9 | { type: 'reset' } 10 | { type: 'setError'; error: string }; 11 12function reducer(state: State, action: Action): State { 13 switch (action.type) { 14 case 'increment': 15 return { ...state, count: state.count + 1 }; 16 case 'decrement': 17 return { ...state, count: state.count - 1 }; 18 case 'reset': 19 return { count: 0, error: null }; 20 case 'setError': 21 return { ...state, error: action.error }; 22 default: 23 return state; 24 } 25} 26 27function Counter() { 28 const [state, dispatch] = useReducer(reducer, { count: 0, error: null }); 29 30 return ( 31 <div> 32 Count: {state.count} 33 <button onClick={() => dispatch({ type: 'increment' })}>+</button> 34 <button onClick={() => dispatch({ type: 'decrement' })}>-</button> 35 <button onClick={() => dispatch({ type: 'reset' })}>Reset</button> 36 </div> 37 ); 38}

Custom Hooks#

1// Fetch hook 2function useFetch<T>(url: string) { 3 const [data, setData] = useState<T | null>(null); 4 const [loading, setLoading] = useState(true); 5 const [error, setError] = useState<Error | null>(null); 6 7 useEffect(() => { 8 const controller = new AbortController(); 9 10 async function fetchData() { 11 try { 12 setLoading(true); 13 const response = await fetch(url, { signal: controller.signal }); 14 const json = await response.json(); 15 setData(json); 16 setError(null); 17 } catch (err) { 18 if (err.name !== 'AbortError') { 19 setError(err as Error); 20 } 21 } finally { 22 setLoading(false); 23 } 24 } 25 26 fetchData(); 27 28 return () => controller.abort(); 29 }, [url]); 30 31 return { data, loading, error }; 32} 33 34// Local storage hook 35function useLocalStorage<T>(key: string, initialValue: T) { 36 const [storedValue, setStoredValue] = useState<T>(() => { 37 try { 38 const item = window.localStorage.getItem(key); 39 return item ? JSON.parse(item) : initialValue; 40 } catch { 41 return initialValue; 42 } 43 }); 44 45 const setValue = (value: T | ((val: T) => T)) => { 46 const valueToStore = value instanceof Function ? value(storedValue) : value; 47 setStoredValue(valueToStore); 48 window.localStorage.setItem(key, JSON.stringify(valueToStore)); 49 }; 50 51 return [storedValue, setValue] as const; 52} 53 54// Debounce hook 55function useDebounce<T>(value: T, delay: number): T { 56 const [debouncedValue, setDebouncedValue] = useState(value); 57 58 useEffect(() => { 59 const timer = setTimeout(() => { 60 setDebouncedValue(value); 61 }, delay); 62 63 return () => clearTimeout(timer); 64 }, [value, delay]); 65 66 return debouncedValue; 67} 68 69// Usage 70function SearchInput() { 71 const [query, setQuery] = useState(''); 72 const debouncedQuery = useDebounce(query, 300); 73 74 const { data, loading } = useFetch( 75 `/api/search?q=${encodeURIComponent(debouncedQuery)}` 76 ); 77 78 return ( 79 <div> 80 <input value={query} onChange={e => setQuery(e.target.value)} /> 81 {loading ? <Spinner /> : <Results data={data} />} 82 </div> 83 ); 84}

Rules of Hooks#

1// ✓ Call at top level 2function Component() { 3 const [count, setCount] = useState(0); 4 const value = useMemo(() => compute(count), [count]); 5 // ... 6} 7 8// ❌ Don't call in conditions 9function BadComponent({ show }) { 10 if (show) { 11 const [count, setCount] = useState(0); // Error! 12 } 13} 14 15// ❌ Don't call in loops 16function BadComponent({ items }) { 17 items.forEach(item => { 18 const [value, setValue] = useState(item); // Error! 19 }); 20} 21 22// ❌ Don't call in nested functions 23function BadComponent() { 24 function handleClick() { 25 const [count, setCount] = useState(0); // Error! 26 } 27}

Best Practices#

Dependencies: ✓ Include all values used inside effect ✓ Use ESLint exhaustive-deps rule ✓ Use refs for values you don't want to track ✓ Consider useReducer for complex state Performance: ✓ Memoize expensive computations ✓ Use useCallback for callbacks passed to children ✓ Split state by update frequency ✓ Don't over-optimize Custom Hooks: ✓ Extract reusable logic ✓ Start with 'use' prefix ✓ Return consistent shapes ✓ Handle cleanup properly

Conclusion#

Hooks enable powerful patterns in React. Understand the rules, manage dependencies carefully, and create custom hooks for reusable logic. Use useMemo and useCallback judiciously—profile first before optimizing.

Share this article

Help spread the word about Bootspring