A Guide to React Design Patterns
🤯 (Me when I saw a component in the company project that was nearly two thousand lines…) All the logic mixed together, having to dig around just to modify one feature, and still needing to locate every call site globally to avoid breaking something else. That’s what happens when design patterns are missing. Good design patterns make code structure clear and easier to maintain, and they’re a necessary step toward becoming a senior developer.
React design patterns are battle-tested ways of organizing code. Mastering these patterns makes your code clearer and easier to maintain.
1. Container Components and Presentational Components
1.1 Core Concept
This pattern separates business logic from UI rendering:
// 展示组件:只负责渲染
function UserList({ users, onDelete }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => onDelete(user.id)}>删除</button>
</li>
))}
</ul>
);
}
// 容器组件:负责数据获取和状态管理
function UserListContainer() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
const handleDelete = async (id) => {
await deleteUser(id);
setUsers(users.filter(u => u.id !== id));
};
return <UserList users={users} onDelete={handleDelete} />;
}
| Trait | Presentational Component | Container Component |
|---|---|---|
| Responsibility | UI rendering | Data logic |
| State | Stateless | Stateful |
| Reusability | High | Low |
2. Higher-Order Components (HOC)
2.1 Basic Usage
A higher-order component is a function that takes a component and returns a new component:
function withLoading(Component) {
return function WithLoading({ isLoading, ...props }) {
if (isLoading) {
return <Spinner />;
}
return <Component {...props} />;
};
}
// 使用
const UserListWithLoading = withLoading(UserList);
function App() {
return <UserListWithLoading isLoading={true} users={[]} />;
}
2.2 Use Case: Permission Control
function withAuth(Component) {
return function AuthenticatedComponent({ isAuthenticated, ...props }) {
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <Component {...props} />;
};
}
const ProtectedRoute = withAuth(Dashboard);
Note: After Hooks arrived, the use cases for HOCs have decreased significantly. Prefer custom Hooks first.
3. Render Props
3.1 Pattern Definition
Share a component’s internal state through a function prop:
function MousePosition({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return render(position);
}
// 使用
<MousePosition render={({ x, y }) => (
<div>鼠标位置: {x}, {y}</div>
)} />
3.2 Compared with HOC
| Feature | HOC | Render Props |
|---|---|---|
| Code organization | Wrapper component | Passing a render function |
| Flexibility | Medium | High |
| TypeScript | More complex | Friendlier |
4. Hooks Pattern
4.1 useState and useEffect
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `计数: ${count}`;
return () => document.title = 'React App';
}, [count]);
return (
<div>
<p>当前计数: {count}</p>
<button onClick={() => setCount(c => c + 1)}>增加</button>
</div>
);
}
4.2 useContext for Cross-Component Communication
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <button className={theme}>主题按钮</button>;
}
4.3 Custom Hooks for Logic Reuse
// 封装数据获取逻辑
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// 使用
function UserProfile({ userId }) {
const { data, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{data.name}</div>;
}
4.4 useReducer for Complex State Management
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<div>
<button onClick={() => dispatch({ type: 'ADD_TODO', text: '新任务' })}>
添加任务
</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
/>
{todo.text}
</li>
))}
</ul>
</div>
);
}
5. Component Composition
5.1 children prop
function Card({ children, title }) {
return (
<div className="card">
{title && <h2>{title}</h2>}
<div className="card-body">{children}</div>
</div>
);
}
function App() {
return (
<Card title="用户信息">
<p>姓名: 张三</p>
<p>邮箱: zhangsan@example.com</p>
</Card>
);
}
5.2 render prop / slot
function Modal({ isOpen, title, children, footer }) {
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content">
<h2>{title}</h2>
<div className="modal-body">{children}</div>
<div className="modal-footer">{footer}</div>
</div>
</div>
);
}
function App() {
return (
<Modal
isOpen={true}
title="确认删除"
footer={
<button onClick={handleConfirm}>确认</button>
}
>
确定要删除这个项目吗?
</Modal>
);
}
6. Error Boundaries
6.1 Implementing Error Boundaries
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>出现错误</div>;
}
return this.props.children;
}
}
// 使用
<ErrorBoundary fallback={<ErrorPage />}>
<MyComponent />
</ErrorBoundary>
Note: Error boundaries only catch errors in child components, not errors within themselves.
7. Controlled and Uncontrolled Components
7.1 Controlled Components
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
7.2 Uncontrolled Components
The state of an uncontrolled component is managed by the DOM itself, and React only retrieves the value through a ref:
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = () => {
console.log(inputRef.current.value);
};
return (
<div>
<input ref={inputRef} defaultValue="初始值" />
<button onClick={handleSubmit}>提交</button>
</div>
);
}
7.3 Key Differences
| Feature | Controlled Component | Uncontrolled Component |
|---|---|---|
| Data source | React state | DOM itself |
| Update method | Via props/onChange | Via ref |
| Initial value | value prop | defaultValue prop |
| Amount of code | More | Less |
// 受控组件:React 完全控制
<input value={value} onChange={e => setValue(e.target.value)} />
// 非受控组件:DOM 控制
<input ref={inputRef} defaultValue="默认值" />
7.4 When to Use Which
Use controlled components:
- When you need to validate input
- When you need to trigger other updates based on the input value
- When you need to disable/format input
- When you need to share state with other components
// 受控组件:实时验证
function ValidatedInput() {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const val = e.target.value;
setValue(val);
setError(val.length < 3 ? '至少需要3个字符' : '');
};
return (
<div>
<input value={value} onChange={handleChange} />
{error && <span className="error">{error}</span>}
</div>
);
}
Use uncontrolled components:
- When you only need the final value
- When you don’t need to process input in real time
- When integrating with third-party libraries
- For simple form submissions
// 非受控组件:简单的表单提交
function SimpleForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const formData = {
name: nameRef.current.value,
email: emailRef.current.value
};
submitToAPI(formData);
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} defaultValue="" />
<input ref={emailRef} defaultValue="" />
<button type="submit">提交</button>
</form>
);
}
7.5 Form scenario comparison
// 场景:搜索输入框
// 搜索建议需要实时更新 → 受控组件
function SearchInput() {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState([]);
useEffect(() => {
if (query) {
fetchSuggestions(query).then(setSuggestions);
}
}, [query]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<SuggestionsList items={suggestions} />
</div>
);
}
// 场景:联系表单
// 只需要提交时获取数据 → 非受控组件
function ContactForm() {
const nameRef = useRef(null);
const messageRef = useRef(null);
const handleSubmit = () => {
sendEmail({
name: nameRef.current.value,
message: messageRef.current.value
});
};
return (
<form>
<input ref={nameRef} />
<textarea ref={messageRef} />
<button onClick={handleSubmit}>发送</button>
</form>
);
}
7.6 Ref forwarding
When you need to attach a ref to a custom component, use forwardRef:
// 父组件
function Parent() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>聚焦输入框</button>
</div>
);
}
// 子组件:使用 forwardRef
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
7.7 Common pitfalls
// ❌ 错误:同时使用 value 和 defaultValue
<input value={value} defaultValue="默认值" />
// ✅ 正确:只使用一种
<input value={value} onChange={handleChange} />
// 或
<input defaultValue="默认值" />
// ❌ 错误:受控组件不提供 onChange
<input value={value} /> // 无法修改!
// ✅ 正确:提供 onChange
<input value={value} onChange={e => setValue(e.target.value)} />
8. Pattern selection guide
graph TD
A[需要复用逻辑] --> B{是否需要状态?}
B -->|是| C[自定义 Hook]
B -->|否| D[纯函数]
A --> E[需要修改渲染逻辑]
E --> F[Render Props]
E --> G[组件复合]
A --> H[需要添加横切关注点]
H --> I[HOC 或 Hook]
9. Summary
Suggestions for choosing React design patterns:
- Prefer Hooks: Custom Hooks are the recommended way to reuse logic in React 16.8+
- Prefer component composition: Build components through composition rather than inheritance
- Choose as needed: Pick the right pattern for the actual scenario; don’t over-engineer