Advanced React Patterns: Context, Custom Hooks & Performance
Overview
As React applications grow, managing state and logic across many components becomes challenging. This tutorial addresses the prop drilling problem with the Context API, shows how to encapsulate reusable logic with Custom Hooks, introduces useReducer for complex state transitions, and explores performance optimisation techniques like memo, useMemo, and useCallback.
1. The Prop Drilling Problem
Prop drilling occurs when you pass data through multiple levels of components just to reach a deeply nested component that needs it. This makes the code harder to maintain and refactor.
React's Context API provides a way to share values without passing props through every level.
2. Context API
The Context API allows you to create a global state that any component in the tree can access directly, without prop drilling.
Steps to use Context
- Create a Context:
const ThemeContext = React.createContext('light'); - Provide the value: Wrap the component tree with
<ThemeContext.Provider value="dark"> - Consume the value: Use the
useContexthook inside any child component.
3. Custom Hooks
Custom hooks allow you to extract component logic into reusable functions.
They are JavaScript functions that start with use and can call
other hooks.
Benefits: Cleaner components, easier testing, and logic sharing across components.
4. useReducer for Complex State
When state logic involves multiple sub‑values or complex transitions,
useReducer is a better alternative than useState.
It's similar to Redux but built‑in.
Use useReducer for state that is complex, nested, or has interdependent updates.
5. Performance: memo, useMemo, useCallback
React.memo
Wraps a component to prevent re‑renders if its props haven't changed. It's a higher‑order component (HOC).
useMemo
Memoizes the result of a computation. It only recalculates when dependencies change.
useCallback
Memoizes a function reference so that it doesn't change on every render. Useful when passing callbacks to memoised children.
6. Lazy Loading & Suspense
React.lazy allows you to dynamically import components, splitting
your bundle and improving initial load time. Suspense provides a
fallback UI while the component loads.
Quiz
Question 1
Which React feature is used to share data across a component tree without passing props manually at every level?
- Props
- State
- Context
- Keys
Show answer
Question 2
What is the naming convention for a custom hook?
- It must start with "use"
- It must end with "Hook"
- It must be all uppercase
- There is no specific convention
Show answer
Question 3
Which hook memoizes a function reference to prevent unnecessary re‑renders in child components?
- useMemo
- useCallback
- useState
- useReducer
Show answer
Exercises
Exercise 1
Create a ThemeContext that holds a theme string ('light' or
'dark') and a function to toggle it. Provide it to the app and consume it in a child
component to change the background color.
Sample answer
Context definition:
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (<ThemeContext.Provider value={{ theme, toggleTheme }}>{children}</ThemeContext.Provider>);
}
Consumer:
function ThemedComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (<div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>...<button onClick={toggleTheme}>Toggle</button></div>);
}
Exercise 2
Write a custom hook useDocumentTitle that updates the browser's
document title. It should accept a string and update the title whenever the string changes.
Sample answer
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}
// Usage: useDocumentTitle('My Awesome Page');
Homework
Homework 1
A UserList component displays a list of users. The list is
filtered by a search term. The search term is stored in a custom hook useSearch
that also persists the term in sessionStorage. Implement the custom hook and
the component that uses it.
Sample answer
Custom hook:
function useSearch(key) {
const [term, setTerm] = useState(() => sessionStorage.getItem(key) || '');
useEffect(() => { sessionStorage.setItem(key, term); }, [key, term]);
return [term, setTerm];
}
Component:
function UserList() {
const [searchTerm, setSearchTerm] = useSearch('userSearch');
const filteredUsers = users.filter(u => u.name.includes(searchTerm));
return (<><input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} /> {filteredUsers.map(...)}</>);
}
Mini‑Project
Shopping Cart with useReducer
Build a simple shopping cart using useReducer:
- State:
{ items: [{ id, name, quantity }], totalItems, totalPrice } - Actions:
addItem,removeItem,increment,decrement. - Display a list of items and a summary (total items and price).
Sample reducer & component
Reducer:
function cartReducer(state, action) {
switch (action.type) {
case 'add':
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) { ... }
return { ... };
case 'remove':
return { items: state.items.filter(i => i.id !== action.payload.id), ... };
default:
return state;
}
}
Component:
function Cart() {
const [state, dispatch] = useReducer(cartReducer, { items: [], totalItems: 0, totalPrice: 0 });
// Render UI with dispatch calls: dispatch({ type: 'add', payload: product })
}
Tutorial Summary
You learned advanced React patterns to build scalable and maintainable
applications. We covered the Context API to solve prop drilling,
custom hooks to encapsulate reusable logic, useReducer for
complex state management, and performance optimisation techniques
(memo, useMemo, useCallback).
The mini‑project brought these concepts together in a real‑world scenario.
Key takeaway: Leveraging these patterns keeps your code clean, efficient, and easier to reason about as your application grows.