
In React, a design pattern is a reusable way to structure components, state, and logic so your app is easier to understand, scale, and maintain. React doesn’t enforce a single pattern—developers choose patterns based on the problem they’re solving.
Below are the most common React design patterns, with clear explanations and examples.
The Container–Presenter pattern separates the behavior of data from its presentation.
As applications grow, mixing logic and UI makes components hard to:
This pattern enforces the Single Responsibility Principle.
┌──────────────┐
│ Container │
│──────────────│
│ State │
│ API Calls │
│ Logic │
└──────┬───────┘
│ props
┌──────▼───────┐
│ Presenter │
│──────────────│
│ JSX / UI │
│ Styling │
└──────────────┘
// UserContainer.jsx
function UserContainer() {
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []);
return (
<UserPresenter
user={user}
loading={loading}
/>
);
}
// UserPresenter.jsx
function UserPresenter({ user, loading }) {
if (loading) return <p>Loading...</p>;
return <h1>Welcome, {user.name}</h1>;
}Defines who owns the form state — React or the DOM.
React fully controls the input value.
The DOM keeps the state, accessed via refs.
Controlled:
User Input → React State → UI
Uncontrolled:
User Input → DOM State// Controlled Input
function ControlledForm() {
const [email, setEmail] = React.useState('');
return (
<input
value={email}
onChange={e => setEmail(e.target.value)}
/>
);
}
// Uncontrolled Input
function UncontrolledForm() {
const inputRef = React.useRef();
const submit = () => alert(inputRef.current.value);
return (
<>
<input ref={inputRef} />
<button onClick={submit}>Submit</button>
</>
);
}Allows components to share state without explicit props implicitly.
<Tabs>
├── <Tabs.List>
├── <Tabs.Tab />
└── <Tabs.Panel />const TabsContext = React.createContext();
function Tabs({ children }) {
const [active, setActive] = React.useState(0);
return (
<TabsContext.Provider value={{ active, setActive }}>
{children}
</TabsContext.Provider>
);
}
function Tab({ index, children }) {
const { setActive } = React.useContext(TabsContext);
return <button onClick={() => setActive(index)}>{children}</button>;
}
function Panel({ index, children }) {
const { active } = React.useContext(TabsContext);
return active === index ? <div>{children}</div> : null;
}A component receives a function prop to decide what to render.
A component receives a function prop to decide what to render.
Component
└── render(data) => JSXfunction MouseTracker({ render }) {
const [pos, setPos] = React.useState({ x: 0, y: 0 });
return (
<div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
{render(pos)}
</div>
);
}
<MouseTracker
render={({ x, y }) => <p>{x}, {y}</p>}
/>A function that takes a component and returns an enhanced component.
Component → HOC → Enhanced Componentfunction withLogger(Component) {
return function Wrapped(props) {
console.log('Props:', props);
return <Component {...props} />;
};
}Extract reusable logic into composable hooks.
Component
└── useCustomHook()function useToggle(initial = false) {
const [value, setValue] = React.useState(initial);
const toggle = () => setValue(v => !v);
return [value, toggle];
}Extract reusable logic into composable hooks.
Component
└── useCustomHook()function useToggle(initial = false) {
const [value, setValue] = React.useState(initial);
const toggle = () => setValue(v => !v);
return [value, toggle];
}Share global state without prop drilling.
<Provider>
└── <App>
└── <Child />const AuthContext = React.createContext();
function AuthProvider({ children }) {
const [user, setUser] = React.useState(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}Allows consumers to override internal state logic.
Action → Reducer → Statefunction reducer(state, action) {
switch (action.type) {
case 'increment': return state + 1;
default: return state;
}
}
Decoupled event‑based communication.
Publisher → Event → Subscribersconst events = {};
export const subscribe = (e, fn) => (events[e] ||= []).push(fn);
export const publish = (e, data) => events[e]?.forEach(fn => fn(data));Reduce unnecessary renders.
const MemoButton = React.memo(Button);Explicit UI placeholders.
<Card>
├── Header
├── Body
└── Footerfunction Card({ header, footer, children }) {
return (
<div>
{header}
{children}
{footer}
</div>
);
}Select behavior dynamically.
function usePayment(type) {
return type === 'paypal' ? usePaypal() : useStripe();
}Hide complexity behind a simple interface.
export const api = {
login: () => fetch('/login'),
logout: () => fetch('/logout')
};Catch rendering errors safely.
class ErrorBoundary extends React.Component {
state = { error: false };
static getDerivedStateFromError() { return { error: true }; }
render() { return this.state.error ? <h1>Error</h1> : this.props.children; }
}Declarative async loading.
<Suspense fallback={<Loader />}>
<Profile />
</Suspense>React Design Patterns provide reusable solutions for structuring components, state, and logic. Use them when your application grows in complexity, and apply only the patterns that solve the problem you are facing.