Skip to main content

Advanced React Patterns

React patterns are reusable solutions to common problems. These patterns help you write more maintainable, flexible, and reusable code.

Compound Components Pattern

Compound components work together to form a complete UI element, sharing implicit state:
import { createContext, useContext, useState } from 'react';

import AccordionItem from './AccordionItem.jsx';
import AccordionTitle from './AccordionTitle.jsx';
import AccordionContent from './AccordionContent.jsx';

const AccordionContext = createContext();

export function useAccordionContext() {
  const ctx = useContext(AccordionContext);

  if (!ctx) {
    throw new Error(
      'Accordion-related components must be wrapped by <Accordion>.'
    );
  }

  return ctx;
}

export default function Accordion({ children, className }) {
  const [openItemId, setOpenItemId] = useState();

  function toggleItem(id) {
    setOpenItemId((prevId) => (prevId === id ? null : id));
  }

  const contextValue = {
    openItemId,
    toggleItem,
  };

  return (
    <AccordionContext.Provider value={contextValue}>
      <ul className={className}>{children}</ul>
    </AccordionContext.Provider>
  );
}

Accordion.Item = AccordionItem;
Accordion.Title = AccordionTitle;
Accordion.Content = AccordionContent;

Using Compound Components

import Accordion from './components/Accordion/Accordion.jsx';

function App() {
  return (
    <main>
      <section>
        <h2>Why work with us?</h2>

        <Accordion className="accordion">
          <Accordion.Item className="accordion-item">
            <Accordion.Title className="accordion-item-title" id="experience">
              We got 20 years of experience
            </Accordion.Title>
            <Accordion.Content
              className="accordion-item-content"
              id="experience"
            >
              <article>
                <p>You can't go wrong with us.</p>
                <p>
                  We are in the business of planning highly individualized
                  vacation trips for more than 20 years.
                </p>
              </article>
            </Accordion.Content>
          </Accordion.Item>
          
          <Accordion.Item className="accordion-item">
            <Accordion.Title id="local-guides" className="accordion-item-title">
              We are working with local guides
            </Accordion.Title>
            <Accordion.Content
              id="local-guides"
              className="accordion-item-content"
            >
              <article>
                <p>We are not doing this along from our office.</p>
                <p>
                  Instead, we are working with local guides to ensure a safe and
                  pleasant vacation.
                </p>
              </article>
            </Accordion.Content>
          </Accordion.Item>
        </Accordion>
      </section>
    </main>
  );
}
Compound components provide a clean, declarative API and keep related components tightly coupled.

Benefits of Compound Components

1

Flexible API

Users can compose components in various ways while maintaining shared state.
2

Separation of Concerns

Each sub-component handles its own rendering logic while sharing context.
3

Implicit State Sharing

State is managed in the parent and automatically available to all children through context.
4

Better Developer Experience

Clear, semantic component structure that’s easy to understand and use.

Render Props Pattern

Pass a function as a prop to share code between components:
export default function SearchableList({ items, itemKeyFn, children }) {
  const [searchTerm, setSearchTerm] = useState('');

  const searchResults = items.filter((item) =>
    JSON.stringify(item).toLowerCase().includes(searchTerm.toLowerCase())
  );

  function handleChange(event) {
    setSearchTerm(event.target.value);
  }

  return (
    <div className="searchable-list">
      <input type="search" placeholder="Search" onChange={handleChange} />
      <ul>
        {searchResults.map((item) => (
          <li key={itemKeyFn(item)}>
            {children(item)}
          </li>
        ))}
      </ul>
    </div>
  );
}

Using Render Props

import SearchableList from './components/SearchableList/SearchableList';

const PLACES = [
  { id: 'p1', title: 'Forest Adventure', image: 'forest.jpg' },
  { id: 'p2', title: 'Beach Paradise', image: 'beach.jpg' },
];

function App() {
  return (
    <main>
      <SearchableList items={PLACES} itemKeyFn={(item) => item.id}>
        {(item) => (
          <article>
            <img src={item.image} alt={item.title} />
            <h3>{item.title}</h3>
          </article>
        )}
      </SearchableList>
    </main>
  );
}
Render props allow the component to be flexible about what it renders while managing the logic internally.

Debouncing Pattern

Delay expensive operations until user stops typing:
import { useRef, useState } from 'react';

export default function SearchableList({ items, itemKeyFn, children }) {
  const lastChange = useRef();
  const [searchTerm, setSearchTerm] = useState('');

  const searchResults = items.filter((item) =>
    JSON.stringify(item).toLowerCase().includes(searchTerm.toLowerCase())
  );

  function handleChange(event) {
    if (lastChange.current) {
      clearTimeout(lastChange.current);
    }

    lastChange.current = setTimeout(() => {
      lastChange.current = null;
      setSearchTerm(event.target.value);
    }, 500);
  }

  return (
    <div className="searchable-list">
      <input type="search" placeholder="Search" onChange={handleChange} />
      <ul>
        {searchResults.map((item) => (
          <li key={itemKeyFn(item)}>{children(item)}</li>
        ))}
      </ul>
    </div>
  );
}
Always clear timeouts when the component unmounts to prevent memory leaks.

Higher-Order Components (HOC)

A function that takes a component and returns a new enhanced component:
function withLoading(Component) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <div>Loading...</div>;
    }
    return <Component {...props} />;
  };
}

// Usage
const UserListWithLoading = withLoading(UserList);

function App() {
  const [isLoading, setIsLoading] = useState(true);
  const [users, setUsers] = useState([]);

  return <UserListWithLoading isLoading={isLoading} users={users} />;
}

Container/Presentational Pattern

Separate logic from presentation:
function UserListContainer() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUsers().then(data => {
      setUsers(data);
      setLoading(false);
    });
  }, []);

  if (loading) return <Loading />;
  
  return <UserList users={users} />;
}

Custom Hook Pattern

Extract reusable logic into custom hooks:
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => {
    setValue(v => !v);
  }, []);

  return [value, toggle];
}

// Usage
function Modal() {
  const [isOpen, toggleOpen] = useToggle(false);

  return (
    <>
      <button onClick={toggleOpen}>Open Modal</button>
      {isOpen && <div>Modal Content</div>}
    </>
  );
}

State Reducer Pattern

Give users control over internal state logic:
function toggleReducer(state, action) {
  switch (action.type) {
    case 'TOGGLE':
      return { on: !state.on };
    case 'RESET':
      return { on: false };
    default:
      return state;
  }
}

function useToggle() {
  const [state, dispatch] = useReducer(toggleReducer, { on: false });

  const toggle = () => dispatch({ type: 'TOGGLE' });
  const reset = () => dispatch({ type: 'RESET' });

  return { on: state.on, toggle, reset };
}

Pattern Comparison

  • Building complex UI components with multiple related parts
  • Need tight coupling between parent and children
  • Want a clean, declarative API
  • Examples: Accordions, Tabs, Select dropdowns
  • Need to share code between components
  • Want flexibility in what gets rendered
  • Logic is reusable but UI varies
  • Examples: Data fetching, animations, form handling
  • Extracting stateful logic
  • Sharing logic across multiple components
  • Simplifying complex components
  • Examples: Form handling, data fetching, subscriptions
  • Need to enhance multiple components with same logic
  • Working with legacy code
  • Cross-cutting concerns like logging, auth
  • Note: Custom hooks are often preferred in modern React

Best Practices

Composition over Inheritance

React favors composition. Build complex UIs from simple, reusable components.

Single Responsibility

Each component should do one thing well. Split complex components into smaller ones.

Props vs Context

Use props for explicit data flow, context for deeply nested data needs.

Keep Components Pure

Same props should always produce same output. Avoid side effects in render.
Source Code: Section 27 - Patterns

Build docs developers (and LLMs) love