原文链接:allthingssmitty.com/2025/12/01/…
作者:Matt Smith
React Hooks问世多年,但多数代码库仍在沿用陈旧模式:零星的 useState 调用、过度使用的 useEffect,以及大量未经深思熟虑的复制粘贴模式。我们都曾深陷其中。
但Hooks的初衷绝非简单替代生命周期方法,而是构建更具表达力、更模块化架构的设计体系。
随着并发 React(React 18/19 时代)的出现,React 处理数据的方式——尤其是异步数据——已发生根本性变革。如今我们拥有服务器组件、use() 函数、服务器操作、框架级数据加载……甚至客户端组件也具备异步能力(取决于具体配置)。
让我们共同探索现代 Hook 模式的演进轨迹:React 正引领开发者走向何方,以及生态系统中持续存在的陷阱。
useEffect陷阱:操作过多,过于频繁
useEffect 仍是使用最频繁的钩子函数。它常沦为逻辑垃圾场,承载着不该在此处理的任务,例如数据获取、衍生值计算,甚至简单的状态转换。这往往导致组件出现"鬼魂效应":在异常时机重新运行,或比预期更频繁地执行。
useEffect(() => { fetchData(); }, [query]); // Re-runs on every query change, even when the new value is effectively the same
这种痛苦主要源于将派生状态与副作用混为一谈,而React对二者的处理方式截然不同。
按React的初衷使用效果
React 在此处的规则出人意料地简单明了:
仅将效果用于实际副作用,即那些与外部世界产生交互的事物。
其余内容应在渲染过程中推导得出。
const filteredData = useMemo(() => { return data.filter(item => item.includes(query)); }, [data, query]);
当你确实需要使用效果时,React 的 useEffectEvent 就是你的好帮手。它让你能在效果内部访问最新的 props/state,而无需扩大依赖数组。
const handleSave = useEffectEvent(async () => { await saveToServer(formData); });
在使用 useEffect 之前,请自问:
- 这是否由外部因素驱动(网络、DOM、订阅)?
- 还是说我可以在渲染期间计算?
如果是后者,使用 useMemo、useCallback 或框架提供的原始工具,将使你的组件变得更少脆弱。
请勿将
useEffectEvent视为规避依赖项数组的捷径。该功能专为在效果器内部运行而优化。
自定义钩子:不仅是可复用性,更是真正的封装性
自定义钩子不仅能减少代码重复,更能将领域逻辑从组件中抽离,让你的用户界面专注于……嗯,用户界面本身。
例如,与其在组件中堆砌设置代码:
useEffect(() => {
const listener = () => setWidth(window.innerWidth);
window.addEventListener('resize', listener);
return () => window.removeEventListener('resize', listener);
}, []);
你可以将其移入一个钩子:
function useWindowWidth() {
const [width, setWidth] = useState(
typeof window !== 'undefined' ? window.innerWidth : 0
);
useEffect(() => {
const listener = () => setWidth(window.innerWidth);
window.addEventListener('resize', listener);
return () => window.removeEventListener('change', listener);
}, []);
return width;
}
更简洁。更易于测试。而且组件不再泄露实现细节。
👉🏻 SSR tip
始终从确定性回退值开始,以避免数据加载不匹配的情况。
基于订阅的状态,使用 useSyncExternalStore 同步
React 18 引入了 useSyncExternalStore 组件,它悄然解决了围绕订阅、撕裂和高频更新的大量问题。
若你曾为 matchMedia、滚动位置或第三方存储在不同渲染场景中表现不一致而困扰,这正是 React 希望你采用的解决方案。
适用场景:
- 浏览器API(
matchMedia、页面可见性、滚动位置) - 外部存储(Redux、Zustand、自定义订阅系统)
- 任何对性能敏感或基于事件驱动的场景
function useMediaQuery(query) {
return useSyncExternalStore(
(callback) => {
const mql = window.matchMedia(query);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
},
() => window.matchMedia(query).matches,
() => false // SSR fallback
);
}
⚠️ 注意:
useSyncExternalStore提供同步更新功能。它并非 useState 的直接替代方案。
更流畅的用户界面:过渡效果与延迟值
当用户输入或筛选时,若应用程序运行迟缓,React 的并发工具可助一臂之力。这些工具虽非魔法,但能帮助 React 在耗时更新与紧急更新之间进行优先级排序。
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);
const filtered = useMemo(() => {
return data.filter(item => item.includes(deferredSearchTerm));
}, [data, deferredSearchTerm]);
输入响应保持流畅,而繁重的过滤工作则被推迟处理。
快速理解模型:
startTransition(() => setState())→ 延迟状态更新useDeferredValue(value)→ 延迟派生值计算
按需组合使用,但切勿过度依赖。这些机制并非用于处理简单计算。
可测试且可调试的钩子
现代 React 开发工具让检查自定义 Hook 变得极其简单。若能合理设计 Hook 结构,多数逻辑无需渲染实际组件即可实现可测试性。
- 保持领域逻辑与 UI 独立
- 尽可能直接测试 Hook
- 将提供者逻辑提取为独立 Hook 以提升清晰度
function useAuthProvider() {
const [user, setUser] = useState(null);
const login = async (credentials) => { /* ... */ };
const logout = () => { /* ... */ };
return { user, login, logout };
}
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const value = useAuthProvider();
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
return useContext(AuthContext);
}
下次调试时你会感谢自己。
超越钩子:迈向数据优先的React应用
React正转向数据优先的渲染流程,尤其随着服务器端组件和基于动作的模式日趋成熟。它并非追求Solid.js那样的精细化响应机制,但React正大力拥抱异步数据和服务器驱动的UI。
值得了解的API:
use():用于渲染期间的异步资源(主要适用于服务器组件;客户端组件通过服务器操作有限支持)useEffectEvent:用于稳定的效果回调useActionState:用于工作流式的异步状态- 框架级缓存与数据原语
- 更优的并发渲染工具与开发工具
方向明确:React希望我们减少对"瑞士军刀式" useEffect 的依赖,转而采用更纯粹的渲染驱动数据流。
围绕派生状态和服务器/客户端边界设计钩子,能让应用自然具备未来适应性。
钩子作为架构,而非语法
Hooks不仅是比类更优雅的API,更是一种架构模式。
- 将派生状态保留在渲染中
- 仅将效果用于实际副作用
- 通过小型专注的Hooks组合逻辑
- 让并发工具平滑异步流程
- 跨越客户端与服务端边界思考
React在进化,我们的Hooks也应随之进化。
若你仍在沿用2020年的钩子编写方式,这无可厚非——多数人亦如此。但React 18+提供了更强大的工具箱,掌握这些模式将迅速带来回报。