React 已经改变了,你的 Hooks 也应该改变

原文: React has changed, your Hooks should too

翻译: 嘿嘿

来源:前端周刊

React Hooks 已经问世多年,但大多数代码库仍然以同样的方式使用它们:用点 useState,过度使用 useEffect,以及大量不经思考就复制粘贴的模式。我们都经历过。

但 Hooks 从来就不是生命周期方法的简单重写。它们是用于构建更具表现力、更模块化架构的设计系统。

随着并发式 React(React 18/19 时代)的到来,React 处理数据(尤其是异步数据 )的方式已经改变。我们现在有了服务器组件、use()、服务器操作、基于框架的数据加载......甚至根据你的设置,在客户端组件中也具备了一些异步能力。

那么,让我们来看看现代 Hook 模式如今是什么样子,React 在引导开发者走向何方,以及生态系统不断陷入的陷阱。

useEffect 陷阱:做得太多、太频繁

useEffect 仍然是最常被滥用的 Hook。它常常成为堆放不应属于那里的逻辑的"垃圾场",例如数据获取、衍生值,甚至简单的状态转换。这通常就是组件开始感觉"诡异"的时候:它们在不恰当的时间重新运行,或者运行得过于频繁。

ts 复制代码
useEffect(() => {
  // 每次查询变化时都会重新运行,即使新值实际上相同
  fetchData();
}, [query]);

这种痛苦大部分源于将衍生状态副作用混在一起,而 React 对这两者的处理方式截然不同。

以 React 预期的方式使用副作用

React 在这里的规则出奇地简单:

只在真正有必要时才使用副作用。

其他一切都应该在渲染过程中衍生出来。

ts 复制代码
const filteredData = useMemo(() => {
  return data.filter(item => item.includes(query));
}, [data, query]);

当你确实需要一个副作用时,React 的 useEffectEvent 会是你的好帮手。它让你能在副作用内部访问最新的 props/状态,而不必扰乱你的依赖数组。

ts 复制代码
const handleSave = useEffectEvent(async () => {
  await saveToServer(formData);
});

在使用 useEffect 之前,先问问自己:

  • 这是由外部因素(网络、DOM、订阅)驱动的吗?
  • 还是我可以在渲染过程中计算这个?

如果是后者,像 useMemouseCallback 或框架提供的基础构建块这样的工具,会让你的组件健壮得多。

🙋🏻‍♂️ 小贴士

不要把 useEffectEvent 当作一种用来逃避编写依赖数组(dependency arrays)的'作弊码'。它是专门针对 Effect 内部的操作逻辑进行优化的。"

自定义 Hooks:不仅仅是复用,更是真正的封装

自定义 Hooks 不仅仅是为了减少重复代码。它们关乎将领域逻辑从组件中抽离出来,让你的 UI 专注于......嗯,UI。

例如,与其用这样的设置代码来污染组件:

ts 复制代码
useEffect(() => {
  const listener = () => setWidth(window.innerWidth);
  window.addEventListener('resize', listener);
  return () => window.removeEventListener('resize', listener);
}, []);

不如将其移入一个 Hook:

ts 复制代码
function useWindowWidth() {
  const [width, setWidth] = useState(
    typeof window !== 'undefined' ? window.innerWidth : 0
  );

  useEffect(() => {
    const listener = () => setWidth(window.innerWidth);
    window.addEventListener('resize', listener);
    // 注意:原文为 'change',但通常 resize 事件应配对 'resize',这里保持原文但应该是笔误
    return () => window.removeEventListener('change', listener);
  }, []);

  return width;
}

这样就干净多了。也更容易测试。你的组件不再泄露实现细节。

SSR 小提示

总是从确定的回退值开始,以避免水合不匹配报错。

基于订阅的状态与 useSyncExternalStore

React 18 引入了 useSyncExternalStore,它悄无声息地解决了一大类与订阅、撕裂效应和高频更新相关的 Bug。

如果你曾经与 matchMedia、滚动位置或跨渲染行为不一致的第三方存储库斗争过,这就是 React 希望你使用的 API。

它适用于:

  • 浏览器 API(matchMedia、页面可见性、滚动位置)
  • 外部存储(Redux、Zustand、自定义订阅系统)
  • 任何对性能敏感或事件驱动的事物
ts 复制代码
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 回退值
  );
}

使用过渡和延迟值实现更流畅的 UI

如果你的应用在用户输入或筛选时感觉卡顿,React 的并发工具可以提供帮助。这些并非魔法,但它们能帮助 React 将紧急更新置于高开销更新之前。

ini 复制代码
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);

const filtered = useMemo(() => {
  return data.filter(item => item.includes(deferredSearchTerm));
}, [data, deferredSearchTerm]);

输入保持响应,而繁重的筛选工作被延后处理。

快速心智模型:

  • startTransition(() => setState()) → 延迟状态更新
  • useDeferredValue(value) → 延迟衍生值

需要时可以一起使用,但不要过度使用。它们不适用于琐碎的计算。

可测试和可调试的 Hooks

现代 React DevTools 让检查自定义 Hooks 变得极其简单。如果你能良好地组织你的 Hooks,大部分逻辑无需渲染实际组件就能测试。

  • 将领域逻辑与 UI 分离
  • 尽可能直接测试 Hooks
  • 为了清晰,将提供者逻辑提取到其自身的 Hook 中
ts 复制代码
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);
}

下次调试时,你会感谢自己这么做。

超越 Hooks:迈向数据优先的 React 应用

React 正朝着数据优先的渲染流程转变,特别是现在服务器组件和基于操作的模式正在成熟。它并非追求像 Solid.js 那样的细粒度响应式,但 React 正大力投入异步数据和服务器驱动的 UI。

值得了解的 API:

  • use() 用于在渲染期间处理异步资源(主要用于服务器组件;通过服务器操作在客户端组件中支持有限)
  • useEffectEvent 用于稳定的副作用回调
  • useActionState 用于类似工作流的异步状态
  • 框架级别的缓存和数据原语
  • 更好的并发渲染工具和 DevTools

方向很明确:React 希望我们减少对"瑞士军刀"式 useEffect 的依赖,更多地依赖简洁、由渲染驱动的数据流。

围绕衍生状态和服务器/客户端边界来设计你的 Hooks,能让你的应用天然地面向未来。

Hooks 即架构,而非语法

Hooks 不仅仅是比类组件更友好的 API,它们是一种架构模式。

  • 将衍生状态放在渲染过程中
  • 只将副作用用于真正的副作用
  • 通过小而专注的 Hooks 组合逻辑
  • 让并发工具平滑处理异步流程
  • 同时考虑客户端服务器边界

React 在进化,我们的 Hooks 也应随之进化。

如果你仍然在用 2020 年的方式写 Hooks,那也没关系。我们大多数人都是如此。但 React 18+ 给了我们一个强大得多的工具箱,熟悉这些模式会很快带来回报。

相关推荐
Codebee2 小时前
A2UI vs OOD全栈方案:AI驱动UI的两种技术路径深度解析
前端·架构
掘金安东尼2 小时前
TypeScript 严格性是非单调的:strict-null-checks 和 no-implicit-any 的相互影响
前端·面试
1024肥宅2 小时前
现代 JavaScript 特性:TypeScript 深度解析与实践
前端·javascript·typescript
麦麦大数据2 小时前
F053 投标推荐可视化系统+推荐算法vue+flask+爬虫
vue.js·爬虫·flask·可视化·推荐算法·招投标
逛逛GitHub2 小时前
推荐 6 个近期火火火的 GitHub 项目
github
用户47949283569152 小时前
并发编程里的"堵车"与"红绿灯":死锁、活锁与两种锁策略(乐观锁、悲观锁)
前端·后端
一 乐2 小时前
智慧医药|基于springboot + vue智慧医药系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端
CC码码2 小时前
告别杂乱数字:用 Intl.NumberFormat 打造全球友好的前端体验
前端·javascript·面试
妮妮喔妮3 小时前
Webpack和Vite优化的区别
前端·webpack·node.js