React Design Patterns for Beginners: Clear, Practical Patterns to Build Maintainable Apps

Updated on
12 min read

A design pattern is a reusable solution to common problems, acting as a blueprint to structure efficient React applications. For beginners entering the world of React, understanding essential design patterns can make a significant difference in developing maintainable and scalable applications. In this guide, you will discover practical patterns and strategies to simplify your decision-making process regarding state management, component design, and more. Here’s what to expect:

  • Thinking in React (component responsibilities and lifting state)
  • Composition (the preferred approach in React)
  • Common component patterns (HOC, Render Props, Compound Components)
  • Hooks and custom hooks
  • State management patterns (local, Context API, external stores)
  • Performance optimizations
  • App architecture and file organization
  • Testing and accessibility
  • Common anti-patterns to avoid
  • A hands-on example app and recipe to try

If you’re using Windows and wish to achieve a Linux-like development environment, consider installing WSL on Windows, which can be beneficial for various React toolchains. For comprehensive details on React concepts and hooks, refer to the official React documentation.


Thinking in React: The Foundation

Embrace the “Thinking in React” approach: break the user interface (UI) into small components, assign each one a single responsibility, and maintain a top-down data flow (props down, events up).

Key Principles:

  • Break UI into components: Identify repeated patterns and distinct pieces of the UI.
  • Single responsibility: Ensure each component fulfills a specific function effectively.
  • Top-down data flow: Parent components should provide data via props, while children should notify parents through callbacks.
  • Lift state up: If multiple components require the same data, elevate that state to the nearest common ancestor.

Component Types:

  • Presentational components: These components focus solely on UI and receive props to render markup, usually as pure functions.
  • Container components: These manage state, fetch data, and pass props to presentational components.

When to Lift State Up:

If two sibling components need the same data, lift that state to their nearest common ancestor. For instance, in a searchable list, both a search input and a display list would share the same query.

Example: Searchable List (Simplified)

function SearchableList({ items }) {
  const [query, setQuery] = useState('');
  const visible = items.filter(i => i.includes(query));

  return (
    <div>
      <Search value={query} onChange={setQuery} />
      <List items={visible} />
    </div>
  );
}

Highlighting smaller components emphasizes reusability; both Search and List are focused and testable.


Composition Over Inheritance — The Core Pattern

React emphasizes composition: build components by combining smaller ones instead of extending classes. Composition allows for flexibility and keeps code loosely coupled.

Common Techniques:

  • Children prop: Pass nested content directly.
  • Render props: Use functions as children to render dynamic content.
  • Composition helpers: Create utility components that accept children or content slots.

Example: Reusable Card Component

function Card({ header, children, footer }) {
  return (
    <div className="card">
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

// Usage
<Card header={<h3>Title</h3>} footer={<small>Meta</small>}>
  <p>Card content</p>
</Card>

Using render props allows for more control:

<Card header={<h3>Title</h3>} footer={<small>Meta</small>}>
  {() => <p>Computed content</p>}
</Card>

The benefits of this approach include enhanced flexibility, easier testing, and decreased coupling compared to inheritance.


Common Component Patterns: HOC, Render Props, and Compound Components

Understanding these patterns is vital. Although modern hooks may replace some use cases, familiarity with these approaches remains beneficial.

Pattern Decision Table:

PatternUse CaseProsCons
Higher-Order Component (HOC)Reuse component logic by wrappingWorks with both class and functional components; supports compositionCan lead to “wrapper hell” and ambiguous props; harder to debug
Render PropsShare logic and control renderingFlexible, explicit ownership of renderingCan create verbose JSX nesting
Compound ComponentsBuild related components that collaborate (e.g., Tabs)Natural API for consumers, encapsulates relationshipsRequires internal coordination via context

Higher-Order Component (HOC)

An HOC is a function that takes a component and returns a new one.

Example: withAuth HOC (Simple)

function withAuth(Component) {
  return function Authenticated(props) {
    const isLoggedIn = useAuth(); // hypothetical hook
    if (!isLoggedIn) return <LoginPrompt />;
    return <Component {...props} />;
  };
}

When utilizing HOCs, be cautious about excessive stacking; prefer hooks where possible.

Render Props

A render prop provides a function as a child to receive data.

function DataProvider({ children }) {
  const [data, setData] = useState(null);
  useEffect(() => { fetchData().then(setData); }, []);
  return children(data);
}

// Usage
<DataProvider>{data => <List items={data} />}</DataProvider>

Compound Components

Ideal for creating naturally grouped APIs (e.g., <Tabs><TabList>…</TabList><TabPanels>…</TabPanels></Tabs>), employing Context for state sharing.

Trade-offs:

Such designs may create clear APIs but necessitate careful coordination and documentation. Many HOC/render prop applications can be replaced by hooks in modern React implementations.

For more patterns and migration insights, refer to the React Patterns Catalog.


Hooks and Custom Hooks — Modern Patterns

Hooks (useState, useEffect, useContext, etc.) have transformed how we share logic; you can now extract reusable logic into custom hooks without relying on HOCs or render props.

Rules of Hooks:

  • Only call hooks at the top level of a function component or custom hook.
  • Only call hooks from React function components or custom hooks.

(For the official rules, view the React documentation: React Hook Rules).

Custom Hooks

Custom hooks encapsulate reusable behaviors, typically prefixed with “use”.

Example: useFetch

import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(r => r.json())
      .then(d => !cancelled && setData(d))
      .finally(() => !cancelled && setLoading(false));
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading };
}

Choosing Between Custom Hooks and HOCs/Render Props

  • Opt for custom hooks to reuse logic within function components.
  • Choose HOCs or render props for component wrapping, class component compatibility, or control over child rendering.

Performance Hooks

  • useMemo, useCallback: Memorize values and callbacks to prevent unnecessary renders.
  • React.memo: Memoizes a component’s rendered output when props are stable.

Example:

const ExpensiveComponent = React.memo(function ({ items, onClick }) {
  // expensive rendering
});

const onClick = useCallback(id => doSomething(id), []);

Utilize these optimizations solely after profiling reveals a need.


State Management Patterns: Local, Lifted, Context, and External Stores

Begin with simplicity and scale up when necessary.

Local State

Utilize useState/useReducer to manage state locally within components, favoring colocated state usage.

Lifted State

When multiple components require the same state, elevate it to their closest common ancestor.

Context API

The Context API allows for passing data down through the component tree without prop drilling. It’s ideal for broader concerns (e.g., theme, locale, auth), but overusing it can result in frequent re-renders if rapidly changing states are included.

Example: Theme Context

const ThemeContext = React.createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Main />
    </ThemeContext.Provider>
  );
}

When to Introduce External Stores

Consider implementing external state management libraries like Redux, Zustand, or Recoil when consistent central stores, complex cross-cutting state, or advanced features (e.g., time travel, devtools) are required. Avoid adding stores merely to evade lifting state; always weigh complexity against benefits.

State Colocation

Aim to keep state close to where it’s consumed to enhance clarity and minimize unnecessary re-renders.

Practical Example: Determining where to store state for forms versus application-wide authentication

  • Form Fields: Retain local state using useReducer/useState solely within the form component.
  • Auth User: Store it in Context or a lightweight state management library if multiple app areas use it.

Performance Patterns and Optimizations

Avoid premature optimization and measure performance utilizing the React DevTools Profiler first. After identifying hotspots, consider these optimizations:

Simple Optimizations:

  • Use React.memo for pure function components.
  • Apply useCallback to maintain stable function references passed to children.
  • Utilize useMemo for computationally expensive values.
  • Refrain from creating inline objects/arrays, which can trigger prop changes with each render.

Code Splitting and Lazy Loading

Utilize React.lazy and Suspense for splitting code at route or component boundaries to minimize the initial bundle size.

Example:

const LazyPage = React.lazy(() => import('./HeavyPage'));

function App() {
  return (
    <Suspense fallback={<Loading/>}>
      <LazyPage />
    </Suspense>
  );
}

Batching and Expensive Computations

Batch state updates effectively and avoid heavy logic in rendering. Move computationally costly tasks into useMemo when applicable.

Performance Checklist:

  • Use profiling to identify slow components.
  • Wrap pure components using React.memo.
  • Stabilize callbacks using useCallback.
  • Memoize computed values using useMemo.
  • Split code for large routes/pages.
  • Keep state local to prevent unnecessary re-renders.

App Architecture & File Organization Patterns

Two Common Folder Layouts:

  1. Type-based: e.g., components/, hooks/, utils/, pages/

    • Pros: Quickly find specific types.
    • Cons: Can scatter feature code across different folders.
  2. Feature-based (recommended for medium to large apps): e.g., features/ or modules/, where each folder contains components, styles, tests, and hooks related to the feature.

    • Pros: Easier understanding of feature boundaries and module extraction.
    • Cons: Slightly more initial setup required.

Example Feature Layout:

src/
  features/
    todos/
      TodoList.jsx
      TodoItem.jsx
      hooks.js
      styles.css
  components/
  hooks/
  utils/

Naming and Component Size:

  • Maintain small, predictable components.
  • Use index.js files for public API re-exports if helpful.
  • Distinguish between public and internal modules through folder structure or naming conventions.

Module Boundaries and Architecture:

Consider the boundaries and APIs for the front end, aligning front-end adapters with back-end design. Explore the ports and adapters architecture for large systems.

For deployment and infrastructure topics (containers, networks), check resources like container networking for deployments and automate infrastructures using Ansible for configuration management.

Regarding repository strategies, evaluate the benefits of monorepo versus multi-repo approaches, guided by the monorepo vs multi-repo strategies.


Testing and Accessibility Patterns

Testing Patterns:

  • Unit Tests: Pure presentational components can be tested as pure functions.
  • Integration Tests: Focus on testing container components and interactions (consider using React Testing Library).
  • E2E Tests: Validate user flows (Cypress or Playwright recommended).

Tools and Guidance:

  • Combine Jest with React Testing Library to test behaviors over implementation details. Refer to Kent C. Dodds’ guidance for insights: Kent C. Dodds’ Testing Blog.
  • Test hooks by wrapping them in a minimal component or utilizing utilities such as testing-library/react-hooks.

Accessibility Patterns:

  • Implement semantic HTML (<button>, <nav>, <header>).
  • Ensure keyboard navigation and focus management are present.
  • Use ARIA roles only when native elements do not suffice.

Quick Accessibility Checklist:

  • Ensure all interactive elements are accessible via keyboard.
  • Confirm proper focus order and visible focus styles are applied.
  • Maintain sufficient color contrast.
  • Ensure images have appropriate alt text.
  • Make sure forms are labeled correctly.

Common Anti-Patterns to Avoid

  • Avoid overuse of Context for every piece of state; not everything needs to be global.
  • Prevent large components doing too much; break them into smaller pieces.
  • Minimize unnecessary refs for managing state/DOM — prefer declarative patterns instead.
  • Avoid direct mutation of props or global objects; maintain immutability of state.
  • Eschew premature optimization; only optimize after profiling indicates it’s necessary.

Patterns in Practice — Small Example Project and Recipes

Example App: A Todo application with theme and persisted settings illustrating various patterns:

  • Composition: Utilizing Card and List components.
  • Custom Hooks: Implementing useTodos (add/remove/toggle) and useLocalStorage for persistence.
  • Context: Using ThemeContext for theming support.
  • State Colocation: Each todo item holds minimal local UI state, such as edit mode.

Recipe — Step-by-Step:

  1. Break the UI into components: App, Header, TodoList, TodoItem, NewTodoForm.
  2. Determine state locations:
    • Implement useTodos hook at the App level, passing down todos and relevant handlers.
    • Control theme using Context.
  3. Create useLocalStorage hook to maintain persisted todos.
  4. Develop small presentational components, placing logic within hooks/containers.
  5. Run tests: unit tests for pure components, integration tests for todo addition/removal functionality.

Starter Templates:

Refactor Checklist (for evolving codebase):

  • Extract repeated logic into custom hooks.
  • Thoughtfully relocate cross-cutting concerns into context or a store.
  • Replace prop-drilling by lifting state up or utilizing Context sparingly.

Conclusion, Further Reading, and Next Steps

Key Takeaways:

  • Favor composition over inheritance; compose small, focused components.
  • Encapsulate reusable logic using hooks and custom hooks.
  • Colocate state where it’s most actively used; lift state only when absolutely necessary.
  • Profile before optimizing; apply React.memo, useCallback, and useMemo as needed.

Suggested Learning Path:

  • Construct small applications (to-do lists, tabs, blogs) and refactor them to extract hooks and components.
  • Review the React documentation: React Docs.
  • Explore community patterns at React Patterns.
  • Gain practical insights on testing and hooks from Kent C. Dodds’ blog: Kent C. Dodds’ Blog.

If you need to present or document your patterns to your team, check out this guide on creating engaging technical presentations.

Try the Example:

Clone or access the example project to practice these refactors: React Design Patterns Starter. Follow this quick checklist on each refactor:

  • Transform repeated logic into a custom hook.
  • Position state closer to where it’s used.
  • Substitute render-props/HOC patterns with hooks where viable.
  • Augment tests for behavior rather than implementation.

Further References:

Additional Internal Resources Mentioned:

Thanks for reading! Build something impactful, refactor it with these patterns, and continually iterate.

TBO Editorial

About the Author

TBO Editorial writes about the latest updates about products and services related to Technology, Business, Finance & Lifestyle. Do get in touch if you want to share any useful article with our community.