React Design Patterns for Beginners: Clear, Practical Patterns to Build Maintainable Apps
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:
Pattern | Use Case | Pros | Cons |
---|---|---|---|
Higher-Order Component (HOC) | Reuse component logic by wrapping | Works with both class and functional components; supports composition | Can lead to “wrapper hell” and ambiguous props; harder to debug |
Render Props | Share logic and control rendering | Flexible, explicit ownership of rendering | Can create verbose JSX nesting |
Compound Components | Build related components that collaborate (e.g., Tabs) | Natural API for consumers, encapsulates relationships | Requires 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:
-
Type-based: e.g., components/, hooks/, utils/, pages/
- Pros: Quickly find specific types.
- Cons: Can scatter feature code across different folders.
-
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
andList
components. - Custom Hooks: Implementing
useTodos
(add/remove/toggle) anduseLocalStorage
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:
- Break the UI into components:
App
,Header
,TodoList
,TodoItem
,NewTodoForm
. - Determine state locations:
- Implement
useTodos
hook at theApp
level, passing down todos and relevant handlers. - Control theme using Context.
- Implement
- Create
useLocalStorage
hook to maintain persisted todos. - Develop small presentational components, placing logic within hooks/containers.
- Run tests: unit tests for pure components, integration tests for todo addition/removal functionality.
Starter Templates:
- Experiment with this starter: React Design Patterns Starter or clone the repository: GitHub Repo.
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
, anduseMemo
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:
- React Official Documentation — Main Concepts & Hooks
- React Patterns Catalog
- Kent C. Dodds — Blog & Testing Patterns
Additional Internal Resources Mentioned:
- Monorepo vs Multi-repo Strategies
- Install WSL on Windows
- Container Networking for Deployments
- Configuration Management with Ansible
- Ports and Adapters Architecture
- Creating Engaging Technical Presentations
Thanks for reading! Build something impactful, refactor it with these patterns, and continually iterate.