Chapter 17: Advanced Hooks & Patterns
These patterns (custom hooks, compound components, render props, and HOCs) are what separate good React developers from great ones. They help you avoid duplication, make components more flexible, and write code that scales beautifully.
We’ll go very slowly and clearly, like I’m sitting next to you in Mumbai with our laptops open, chai on the table, and I’m showing you live examples you can copy-paste right now.
Let’s dive in! 🚀
1. Custom Hooks – Reusing Stateful Logic
Custom hooks are just normal functions that start with use and can call other hooks inside them. They let you extract and reuse stateful logic across components.
Example 1: 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 |
// src/hooks/useLocalStorage.ts import { useState, useEffect } from 'react'; export function useLocalStorage<T>(key: string, initialValue: T) { // Get from localStorage on first render const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } }); // Save to localStorage whenever value changes useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(storedValue)); } catch (error) { console.error(error); } }, [key, storedValue]); return [storedValue, setStoredValue] as const; } |
Usage anywhere:
|
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 |
import { useLocalStorage } from '../hooks/useLocalStorage'; 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', border: 'none', borderRadius: '8px' }} > Toggle Theme (persists after refresh!) </button> ); } |
Example 2: useFetch (Data fetching hook)
|
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 |
// src/hooks/useFetch.ts import { useState, useEffect } from 'react'; export function useFetch<T>(url: string) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchData = async () => { try { const res = await fetch(url); if (!res.ok) throw new Error('Network response was not ok'); const result = await res.json(); setData(result); } catch (err) { setError(err instanceof Error ? err.message : 'Something went wrong'); } finally { setLoading(false); } }; fetchData(); }, [url]); return { data, loading, error }; } |
Usage:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function UsersPage() { const { data, loading, error } = useFetch<User[]>('https://jsonplaceholder.typicode.com/users'); if (loading) return <div>Loading users...</div>; if (error) return <div>Error: {error}</div>; return ( <ul> {data?.map(user => <li key={user.id}>{user.name}</li>)} </ul> ); } |
2. Compound Components – Flexible & Composable UI
Compound components let you create flexible APIs where child components implicitly share state through context.
Example: Accordion (only one section open at a time)
|
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 |
// src/components/Accordion.tsx import { createContext, useContext, useState, ReactNode } from 'react'; interface AccordionContextType { activeIndex: number | null; toggle: (index: number) => void; } const AccordionContext = createContext<AccordionContextType | undefined>(undefined); function useAccordionContext() { const context = useContext(AccordionContext); if (!context) throw new Error('Accordion components must be used inside Accordion'); return context; } // Parent component export function Accordion({ children }: { children: ReactNode }) { const [activeIndex, setActiveIndex] = useState<number | null>(null); const toggle = (index: number) => { setActiveIndex(activeIndex === index ? null : index); }; return ( <AccordionContext.Provider value={{ activeIndex, toggle }}> <div style={{ border: '1px solid #ddd', borderRadius: '8px', overflow: 'hidden' }}> {children} </div> </AccordionContext.Provider> ); } // Child: AccordionItem export function AccordionItem({ index, title, children }: { index: number; title: string; children: ReactNode; }) { const { activeIndex, toggle } = useAccordionContext(); const isOpen = activeIndex === index; return ( <div> <button onClick={() => toggle(index)} style={{ width: '100%', padding: '16px', background: isOpen ? '#646cff' : '#f0f4ff', color: isOpen ? 'white' : 'black', border: 'none', textAlign: 'left', fontSize: '18px', cursor: 'pointer' }} > {title} {isOpen ? '▲' : '▼'} </button> {isOpen && ( <div style={{ padding: '20px', background: '#fff' }}> {children} </div> )} </div> ); } // Usage function App() { return ( <Accordion> <AccordionItem index={0} title="What is React?"> React is a JavaScript library for building user interfaces. </AccordionItem> <AccordionItem index={1} title="Why use hooks?"> Hooks let you use state and other React features without classes. </AccordionItem> <AccordionItem index={2} title="What are compound components?"> They share state implicitly through context — very flexible! </AccordionItem> </Accordion> ); } |
3. Render Props Pattern – Share Logic via Function Children
Render props = pass a function as a child that receives shared state/logic.
Example: Mouse Tracker
|
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 |
import { useState, ReactNode } from 'react'; interface MousePosition { x: number; y: number; } function MouseTracker({ render }: { render: (mouse: MousePosition) => ReactNode }) { const [mouse, setMouse] = useState<MousePosition>({ x: 0, y: 0 }); const handleMouseMove = (e: React.MouseEvent) => { setMouse({ x: e.clientX, y: e.clientY }); }; return ( <div onMouseMove={handleMouseMove} style={{ height: '300px', background: '#f0f4ff', position: 'relative' }} > {render(mouse)} </div> ); } // Usage function App() { return ( <MouseTracker render={({ x, y }) => ( <> <h2>Mouse Tracker (Render Props)</h2> <p>Mouse at: ({x}, {y})</p> <div style={{ position: 'absolute', left: x - 10, top: y - 10, width: '20px', height: '20px', background: '#646cff', borderRadius: '50%', pointerEvents: 'none' }} /> </> )} /> ); } |
4. Higher-Order Components (HOCs) – Wrapping Components
HOC = function that takes a component and returns a new enhanced component.
Example: withAuth HOC
|
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 |
import { ComponentType } from 'react'; interface AuthProps { isAuthenticated: boolean; user: { name: string } | null; } function withAuth<P extends object>(WrappedComponent: ComponentType<P & AuthProps>) { return function EnhancedComponent(props: P) { // Simulate auth check const isAuthenticated = true; const user = { name: 'Webliance' }; if (!isAuthenticated) { return <div>Please login to access this page</div>; } return <WrappedComponent {...props} isAuthenticated user={user} />; }; } // Usage function Profile({ isAuthenticated, user }: AuthProps) { return ( <div> <h2>Welcome, {user?.name}!</h2> </div> ); } const ProtectedProfile = withAuth(Profile); |
Summary – Chapter 17 Key Takeaways
- Custom hooks → extract reusable stateful logic (useFetch, useLocalStorage…)
- Compound components → flexible, composable UIs (Accordion, Tabs, Select…)
- Render props → share logic via function children (MouseTracker, Data fetching…)
- HOCs → enhance components (withAuth, withLoading, withRouter…)
Mini Homework
- Create a custom hook useToggle that returns [isOn, toggle]
- Build a compound Tabs component (like Accordion but horizontal tabs)
- Bonus: Make a withLoading HOC that shows a spinner while data is loading
