主题模式
React Hooks 设计模式与最佳实践
React Hooks 自 16.8 版本发布以来,彻底改变了我们编写 React 组件的方式。从类组件到函数组件的转变不仅仅是语法的简化,更是一种全新的组件设计思维。本文将深入探讨 React Hooks 的设计模式与最佳实践,帮助你写出更优雅、更高效的 React 代码。
一、Hooks 的本质:状态与副作用的封装
1.1 为什么需要 Hooks?
在 Hooks 出现之前,React 组件面临几个核心问题:
jsx
// 类组件的问题:逻辑分散
class Profile extends React.Component {
constructor(props) {
super(props);
this.state = { user: null, loading: true };
this.fetchUser = this.fetchUser.bind(this);
}
componentDidMount() {
this.fetchUser();
window.addEventListener('resize', this.handleResize);
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
fetchUser() { /* ... */ }
handleResize() { /* ... */ }
render() {
const { user, loading } = this.state;
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
}问题分析:
- 逻辑分散:相关的逻辑被拆分到多个生命周期方法中
- 代码冗长:需要大量的样板代码(constructor、bind 等)
- 复用困难:状态逻辑难以复用,只能通过 HOC 或 render props
1.2 Hooks 的设计哲学
Hooks 的核心思想是将相关联的逻辑组织在一起,而不是按生命周期分割:
jsx
// 函数组件:逻辑聚合
function Profile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [userId]);
// 窗口resize监听 - 独立的副作用
useEffect(() => {
const handleResize = () => console.log('resized');
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}优势:
- 正确 相关逻辑聚合在一起
- 正确 无需手动绑定 this
- 正确 副作用可独立管理
- 正确 易于测试和复用
二、核心 Hooks 深度解析
2.1 useState:状态管理的基础
基础用法与常见陷阱
jsx
function Counter() {
// 正确 正确:初始值只会在首次渲染时使用
const [count, setCount] = useState(0);
// 错误 错误:每次渲染都会创建新数组
const [items, setItems] = useState(createExpensiveArray());
// 正确 正确:惰性初始化,只执行一次
const [items, setItems] = useState(() => createExpensiveArray());
// 正确 正确:函数式更新,避免闭包陷阱
const increment = () => setCount(prev => prev + 1);
// 批量更新
const handleClick = () => {
// React 18+ 自动批处理
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 最终 count 增加 2
};
return <button onClick={increment}>{count}</button>;
}对象状态的最佳实践
jsx
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
// 错误 错误:直接修改状态
const updateName = (name) => {
user.name = name;
setUser(user); // 不会触发重新渲染
};
// 正确 正确:创建新对象
const updateUser = (field, value) => {
setUser(prev => ({ ...prev, [field]: value }));
};
// 正确 更好:使用 useReducer 处理复杂状态
const [state, dispatch] = useReducer(userReducer, initialUser);
}2.2 useEffect:副作用管理艺术
依赖数组的正确理解
jsx
function DataFetcher({ userId, filter }) {
const [data, setData] = useState(null);
// 错误 缺少依赖:可能的 bug
useEffect(() => {
fetchData(userId, filter).then(setData);
}, [userId]); // filter 变化时不会重新执行
// 正确 完整依赖
useEffect(() => {
fetchData(userId, filter).then(setData);
}, [userId, filter]);
// 正确 使用函数式更新避免依赖
useEffect(() => {
fetchData(userId).then(data => {
setData(prev => ({ ...prev, ...data })); // 不依赖当前 data
});
}, [userId]);
// 正确 空依赖数组:只在挂载时执行
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(timer);
}, []);
// 正确 无依赖数组:每次渲染后都执行
useEffect(() => {
console.log('rendered');
});
}清理函数的正确使用
jsx
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// 正确 使用 AbortController 取消请求
const controller = new AbortController();
if (query) {
searchAPI(query, { signal: controller.signal })
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
}
return () => controller.abort();
}, [query]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}2.3 useMemo & useCallback:性能优化的双刃剑
jsx
function OptimizedComponent({ items, onClick }) {
// 错误 过度优化:简单计算不需要 memo
const doubled = useMemo(() => count * 2, [count]);
// 正确 合理优化:昂贵计算
const sortedItems = useMemo(() => {
return items.sort((a, b) => {
// 复杂的排序逻辑...
return compareFn(a, b);
});
}, [items]);
// 错误 无意义的 memo:函数每次都会创建新引用
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 空依赖,但组件内定义的函数还是新引用
// 正确 合理使用:作为子组件 props 传递
const handleItemClick = useCallback((id) => {
onClick(id);
}, [onClick]); // 依赖 onClick,避免过时闭包
return (
<ExpensiveChild
items={sortedItems}
onClick={handleItemClick}
/>
);
}
// 配合 React.memo 使用
const ExpensiveChild = React.memo(function ExpensiveChild({ items, onClick }) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});性能优化原则:
- 先写正确的代码,再考虑优化
- 使用 React DevTools Profiler 识别性能瓶颈
- useMemo/useCallback 有成本,不要滥用
三、自定义 Hooks:逻辑复用的利器
3.1 基本模式:提取状态逻辑
jsx
// 自定义 Hook:窗口尺寸
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// 使用
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
窗口尺寸: {width} x {height}
{width < 768 && <MobileNav />}
</div>
);
}3.2 高级模式:返回操作方法
jsx
// 自定义 Hook:本地存储状态
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function
? value(storedValue)
: value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// 使用
function TodoApp() {
const [todos, setTodos] = useLocalStorage('todos', []);
const addTodo = (text) => {
setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
};
return <TodoList todos={todos} onAdd={addTodo} />;
}3.3 组合模式:Hooks 组合
jsx
// 自定义 Hook:数据获取(带缓存)
function useFetch(url, options = {}) {
const cache = useRef(new Map());
const [state, setState] = useState({
data: null,
loading: true,
error: null
});
useEffect(() => {
let cancelled = false;
// 检查缓存
if (cache.current.has(url)) {
setState({ data: cache.current.get(url), loading: false, error: null });
return;
}
// 获取数据
fetch(url, options)
.then(res => res.json())
.then(data => {
if (!cancelled) {
cache.current.set(url, data);
setState({ data, loading: false, error: null });
}
})
.catch(error => {
if (!cancelled) {
setState({ data: null, loading: false, error });
}
});
return () => { cancelled = true; };
}, [url, JSON.stringify(options)]);
return state;
}
// 组合多个 Hooks
function useUser(userId) {
const { data: user, loading: userLoading } = useFetch(`/api/users/${userId}`);
const { data: posts, loading: postsLoading } = useFetch(`/api/users/${userId}/posts`);
const { data: followers } = useFetch(`/api/users/${userId}/followers`);
return {
user,
posts,
followers,
loading: userLoading || postsLoading
};
}四、高级设计模式
4.1 状态提升与下钻
jsx
// 状态提升:共享状态
function App() {
const [user, setUser] = useState(null);
return (
<Router>
<Navbar user={user} />
<Routes>
<Route path="/" element={<Home user={user} />} />
<Route path="/profile" element={<Profile user={user} setUser={setUser} />} />
</Routes>
</Router>
);
}
// 避免过度下钻:使用 Context
const UserContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Router>
<Navbar />
<Routes>...</Routes>
</Router>
</UserContext.Provider>
);
}
// 任意深度的组件都可以访问
function Profile() {
const { user, setUser } = useContext(UserContext);
return <div>{user?.name}</div>;
}4.2 Reducer 模式:复杂状态管理
jsx
// 状态机思维
const formReducer = (state, action) => {
switch (action.type) {
case 'FIELD_CHANGE':
return {
...state,
values: { ...state.values, [action.field]: action.value },
errors: { ...state.errors, [action.field]: '' }
};
case 'SUBMIT_START':
return { ...state, isSubmitting: true, submitError: null };
case 'SUBMIT_SUCCESS':
return { ...state, isSubmitting: false, submitted: true };
case 'SUBMIT_ERROR':
return { ...state, isSubmitting: false, submitError: action.error };
case 'RESET':
return action.initialState;
default:
return state;
}
};
function useForm(initialValues, onSubmit) {
const [state, dispatch] = useReducer(formReducer, {
values: initialValues,
errors: {},
isSubmitting: false,
submitError: null,
submitted: false
});
const handleChange = (field) => (e) => {
dispatch({ type: 'FIELD_CHANGE', field, value: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SUBMIT_START' });
try {
await onSubmit(state.values);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (error) {
dispatch({ type: 'SUBMIT_ERROR', error: error.message });
}
};
return { ...state, handleChange, handleSubmit };
}4.3 Ref 模式:命令式操作
jsx
// useImperativeHandle:暴露特定方法
const Modal = forwardRef(function Modal({ children, onClose }, ref) {
const dialogRef = useRef(null);
useImperativeHandle(ref, () => ({
open: () => dialogRef.current?.showModal(),
close: () => dialogRef.current?.close()
}));
return (
<dialog ref={dialogRef} onClose={onClose}>
{children}
<button onClick={() => dialogRef.current?.close()}>关闭</button>
</dialog>
);
});
// 使用
function App() {
const modalRef = useRef(null);
return (
<div>
<button onClick={() => modalRef.current?.open()}>打开弹窗</button>
<Modal ref={modalRef}>
<h2>弹窗内容</h2>
</Modal>
</div>
);
}五、常见陷阱与最佳实践
5.1 闭包陷阱
jsx
function Counter() {
const [count, setCount] = useState(0);
// 错误 闭包陷阱:count 始终是初始值 0
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 永远是 0
setCount(count + 1); // 设置为 1
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖:count 是过时闭包
// 正确 解决方案 1:添加依赖
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count]);
// 正确 解决方案 2:函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 总是获取最新值
}, 1000);
return () => clearInterval(timer);
}, []);
// 正确 解决方案 3:使用 ref
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setInterval(() => {
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
}5.2 竞态条件
jsx
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let cancelled = false;
const sequence = useRef(0);
const currentSequence = ++sequence.current;
searchAPI(query).then(data => {
// 正确 检查是否已取消或是否是最新请求
if (!cancelled && currentSequence === sequence.current) {
setResults(data);
}
});
return () => { cancelled = true; };
}, [query]);
return <ResultList results={results} />;
}5.3 最佳实践清单
jsx
function BestPracticesExample({ userId }) {
// 正确 1. Hooks 在组件顶层调用
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 正确 2. 自定义 Hooks 以 use 开头
const { data, error } = useUser(userId);
// 正确 3. 使用解构和默认值
const { name = 'Guest', avatar = defaultAvatar } = user || {};
// 正确 4. 条件渲染在 return 中处理
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
// 正确 5. 使用 key 重置组件状态
return (
<div>
<UserForm key={userId} user={user} />
</div>
);
// 错误 不要在循环、条件中调用 Hooks
// 错误 不要在普通函数中调用 Hooks
// 错误 不要直接修改 state
}六、Hooks 未来展望
React 19 带来了更多激动人心的 Hooks:
jsx
// use:资源获取
function Note({ id }) {
const note = use(fetchNote(id)); // 暂停渲染直到 Promise resolve
return <div>{note.title}</div>;
}
// useOptimistic:乐观更新
function TodoList({ todos }) {
const [optimisticTodos, addTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function handleSubmit(formData) {
addTodo({ id: Date.now(), text: formData.get('text') });
await saveTodo(formData);
}
return (
<form action={handleSubmit}>
{optimisticTodos.map(todo => (
<div key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</div>
))}
<input name="text" />
<button type="submit">添加</button>
</form>
);
}总结
React Hooks 的精髓在于:
- 逻辑聚合:相关代码组织在一起,而非分散在生命周期中
- 声明式思维:描述"是什么"而非"怎么做"
- 组合优于继承:通过自定义 Hooks 实现逻辑复用
- 关注点分离:状态管理、副作用、性能优化各司其职
掌握 Hooks 不仅是学习新的 API,更是拥抱一种全新的 React 编程范式。在实际项目中,始终遵循 React 规则,保持 Hooks 的纯净和可预测性,你就能写出优雅、高效的 React 代码。
本文由小虾子 撰写