· 7 min read
React 设计模式指北
React
🤯(看到公司项目中一个组件将近两千行时的我belike…)所有逻辑混在一起,修改一个功能要翻半天,还得全局定位调用点,避免影响到其它功能,这就是缺乏设计模式的后果。良好的设计模式能让代码结构清晰、易于维护,也是进阶高级开发的必经之路。
React 设计模式是经过实践验证的代码组织方式。掌握这些模式,能让代码更清晰、更易维护。
1. 容器组件与展示组件
1.1 核心概念
这种模式将业务逻辑和 UI 呈现分离:
// 展示组件:只负责渲染
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} />;
}
| 特点 | 展示组件 | 容器组件 |
|---|---|---|
| 职责 | UI 渲染 | 数据逻辑 |
| 状态 | 无状态 | 有状态 |
| 复用性 | 高 | 低 |
2. 高阶组件(HOC)
2.1 基本用法
高阶组件是接收组件,返回新组件的函数:
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 应用场景:权限控制
function withAuth(Component) {
return function AuthenticatedComponent({ isAuthenticated, ...props }) {
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <Component {...props} />;
};
}
const ProtectedRoute = withAuth(Dashboard);
注意: Hooks 出现后,HOC 的使用场景减少了很多。优先考虑自定义 Hook。
3. 渲染属性(Render Props)
3.1 模式定义
通过函数 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 对比 HOC
| 特性 | HOC | Render Props |
|---|---|---|
| 代码组织 | 包装组件 | 传递渲染函数 |
| 灵活性 | 中 | 高 |
| TypeScript | 较复杂 | 更友好 |
4. Hooks 模式
4.1 useState 和 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 实现跨组件通信
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 自定义 Hook 复用逻辑
// 封装数据获取逻辑
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 管理复杂状态
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. 组件复合(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. 错误边界
6.1 实现错误边界
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>
注意: 错误边界只捕获子组件的错误,不能捕获自身的错误。
7. 受控与非受控组件
7.1 受控组件
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
7.2 非受控组件
非受控组件的状态由 DOM 自身管理,React 只通过 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 核心区别
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据来源 | React 状态 | DOM 自身 |
| 更新方式 | 通过 props/onChange | 通过 ref 获取 |
| 初始值 | value prop | defaultValue prop |
| 代码量 | 较多 | 较少 |
// 受控组件:React 完全控制
<input value={value} onChange={e => setValue(e.target.value)} />
// 非受控组件:DOM 控制
<input ref={inputRef} defaultValue="默认值" />
7.4 何时使用哪种
使用受控组件:
- 需要对输入进行验证
- 需要根据输入值触发其他更新
- 需要禁用/格式化输入
- 需要与其他组件共享状态
// 受控组件:实时验证
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>
);
}
使用非受控组件:
- 只需要获取最终值
- 不需要实时处理输入
- 与第三方库集成
- 简单的表单提交
// 非受控组件:简单的表单提交
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 表单场景对比
// 场景:搜索输入框
// 搜索建议需要实时更新 → 受控组件
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 转发
当需要为自定义组件添加 ref 时,使用 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 常见陷阱
// ❌ 错误:同时使用 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. 模式选择指北
graph TD
A[需要复用逻辑] --> B{是否需要状态?}
B -->|是| C[自定义 Hook]
B -->|否| D[纯函数]
A --> E[需要修改渲染逻辑]
E --> F[Render Props]
E --> G[组件复合]
A --> H[需要添加横切关注点]
H --> I[HOC 或 Hook]
9. 总结
React 设计模式的选择建议:
- 优先使用 Hooks: 自定义 Hook 是 React 16.8+ 推荐的逻辑复用方式
- 组件复合优先: 优先通过组合而非继承构建组件
- 按需选择: 根据实际场景选择合适的模式,不要过度设计