Tutorial 2.1.2

Advanced React Patterns: Context, Custom Hooks & Performance

Chapter 4 · Frontend Development
~3 hours Advanced 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.

Why this matters: These patterns are essential for building scalable, maintainable, and high‑performance React applications at the enterprise level.

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.

// Problem: Passing 'theme' through many levels <App theme="dark"> <Layout theme={theme}> <Header theme={theme}> <Nav theme={theme} /> // Only Nav actually uses 'theme' </Header> </Layout> </App>

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 useContext hook inside any child component.
// ThemeContext.js export const ThemeContext = createContext(); // App.js (Provider) function App() { return ( <ThemeContext.Provider value="dark"> <Layout /> </ThemeContext.Provider> ); } // Nav.js (Consumer) function Nav() { const theme = useContext(ThemeContext); return <nav className={theme}>...</nav>; }
Tip: Wrap the context provider in a custom provider component that holds the state and provides a dispatch function for updates.

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.

// Custom hook: useLocalStorage function useLocalStorage(key, initialValue) { const [value, setValue] = useState(() => { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : initialValue; }); useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); return [value, setValue]; } // Usage in a component function App() { const [name, setName] = useLocalStorage('name', 'Guest'); return <input value={name} onChange={e => setName(e.target.value)} />; }

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.

function counterReducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: return state; } } function Counter() { const [state, dispatch] = useReducer(counterReducer, { count: 0 }); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </> ); }

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.

function ExpensiveComponent({ data, onSave }) { // ... } // Memoize the component const MemoizedComponent = React.memo(ExpensiveComponent); function Parent() { const [count, setCount] = useState(0); // Memoize expensive calculation const expensiveValue = useMemo(() => { return heavyComputation(count); }, [count]); // Memoize callback const handleSave = useCallback(() => { console.log('Saving...'); }, []); // Empty deps = never changes return <MemoizedComponent data={expensiveValue} onSave={handleSave} />; }

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.

const LazyComponent = React.lazy(() => import('./LazyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> ); }

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
C. Context.

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
A. It must start with "use". This is a linting rule enforced by ESLint.

Question 3

Which hook memoizes a function reference to prevent unnecessary re‑renders in child components?

  • useMemo
  • useCallback
  • useState
  • useReducer
Show answer
B. useCallback.

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.