【翻译】React 已经改变,你的 Hooks 也该改变

25 阅读6分钟

原文链接: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、订阅)?
  • 还是说我可以在渲染期间计算?

如果是后者,使用 useMemouseCallback 或框架提供的原始工具,将使你的组件变得更少脆弱。

请勿将 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+提供了更强大的工具箱,掌握这些模式将迅速带来回报。