吴文俊
吴文俊
关注前端编码,学习 Java, Android-iOS App.
冬天、冬梦、冬季景观-https://pixabay.com/zh/photos/winter-winter-dream-winter-landscape-4708076/

React 已经改变,Hooks 也应该如此

现代 React 已经超越了过度使用 useEffect 的阶段。探索 2026 年能够实现更清晰逻辑、更少 bug 和可扩展架构的 Hook 模式。

React Hooks 已经存在多年,但大多数代码库仍然以相同的方式使用它们:一些 useState、一个过载的 useEffect,以及大量不经思考就复制粘贴的模式。我们都经历过。

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

随着并发 React(React 18/19 时代)的出现,React 处理数据的方式,特别是异步数据,已经发生了变化。我们现在有了服务端组件、use()、服务端操作、基于框架的数据加载……甚至在客户端组件中也有一些异步功能,具体取决于你的设置。

让我们来看看现代 Hook 模式在今天是什么样子,React 在引导开发者走向何方,以及生态系统持续遇到的陷阱。

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

useEffect 仍然是最常被滥用的 hook。它常常成为不属于这里的逻辑的倾倒场,例如数据获取、派生值,甚至是简单的状态转换。这通常会让组件开始感觉”闹鬼”:它们在奇怪的时候重新运行,或者比应该运行的频率更高。

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

这种困扰主要源于将派生状态副作用 混在一起,而 React 对它们的处理方式完全不同。

按照 React 的预期使用 effects

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

除了副作用外,其他所有内容都应该在渲染期间派生。

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

当你真正需要一个 effect 时,React 的 useEffectEvent 是你的好帮手。它允许你在 effect 中访问最新的 props/state,而不会破坏你的依赖数组。

const handleSave = useEffectEvent(async () => {
  await saveToServer(formData);
});

在触及 useEffect 之前,问问自己:

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

如果是后者,像 useMemouseCallback 或框架提供的原语会让你的组件更加健壮。

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

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

例如,与其用这样的设置代码让组件变得杂乱:

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

你可以将其移到一个 Hook 中:

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;
}

更清晰。更易于测试。你的组件不再泄露实现细节。

使用 useSyncExternalStore 的基于订阅的状态

React 18 引入了 useSyncExternalStore,它悄悄地解决了订阅、撕裂和高频更新的一大类 bug。

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

用于:

  • 浏览器 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 回退方案
  );
}

使用 transitions 和 deferred values 实现更流畅的 UI

如果当用户输入或过滤时你的应用感觉迟钝,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) → 延迟 派生值

在需要时将它们一起使用,但不要过度。这些不是用于微不足道的计算。

可测试且可调试的 Hooks

现代 React DevTools 让检查自定义 Hooks 变得异常简单。如果你很好地构建你的 Hooks,大部分逻辑变得可以在不渲染实际组件的情况下进行测试。

  • 将领域逻辑与 UI 分离
  • 尽可能直接测试 Hooks
  • 将 provider 逻辑提取到自己的 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);
}

下次你必须调试它时,你会感谢自己。

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

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

值得了解的 API:

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

方向很明确:React 希望我们减少对”瑞士军刀”式 useEffect 使用的依赖,更多地依赖清晰的渲染驱动的数据流。

围绕派生状态和服务端/客户端边界设计你的 Hooks,可以让你的应用自然地面向未来。

Hooks 作为架构,而非语法

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

  • 保持派生状态在渲染中
  • 仅将 effects 用于实际的副作用
  • 通过小型、专注的 Hooks 组合逻辑
  • 让并发工具平滑异步流程
  • 跨越客户端和服务端边界进行思考

React 正在演进。我们的 Hooks 也应该随之演进。

如果你仍然像 2020 年那样编写 Hooks,那也没关系。我们大多数人都是如此。但 React 18+ 为我们提供了更好的工具箱,熟悉这些模式会很快得到回报。现代 React 已经超越了过度使用 useEffect 的阶段。探索 2026 年能够实现更清晰逻辑、更少 bug 和可扩展架构的 Hook 模式。

React Hooks 已经存在多年,但大多数代码库仍然以相同的方式使用它们:一些 useState、一个过载的 useEffect,以及大量不经思考就复制粘贴的模式。我们都经历过。

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

随着并发 React(React 18/19 时代)的出现,React 处理数据的方式,特别是异步数据,已经发生了变化。我们现在有了服务端组件、use()、服务端操作、基于框架的数据加载……甚至在客户端组件中也有一些异步功能,具体取决于你的设置。

让我们来看看现代 Hook 模式在今天是什么样子,React 在引导开发者走向何方,以及生态系统持续遇到的陷阱。

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

useEffect 仍然是最常被滥用的 hook。它常常成为不属于这里的逻辑的倾倒场,例如数据获取、派生值,甚至是简单的状态转换。这通常会让组件开始感觉”闹鬼”:它们在奇怪的时候重新运行,或者比应该运行的频率更高。

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

这种困扰主要源于将派生状态副作用混在一起,而 React 对它们的处理方式完全不同。

按照 React 的预期使用 effects

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

除了副作用外,其他所有内容都应该在渲染期间派生。

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

当你真正需要一个 effect 时,React 的 useEffectEvent 是你的好帮手。它允许你在 effect 中访问最新的 props/state,而不会破坏你的依赖数组。

const handleSave = useEffectEvent(async () => {
  await saveToServer(formData);
});

在触及 useEffect 之前,问问自己:

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

如果是后者,像 useMemouseCallback 或框架提供的原语会让你的组件更加健壮。

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

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

例如,与其用这样的设置代码让组件变得杂乱:

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

你可以将其移到一个 Hook 中:

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;
}

更清晰。更易于测试。你的组件不再泄露实现细节。

使用 useSyncExternalStore 的基于订阅的状态

React 18 引入了 useSyncExternalStore,它悄悄地解决了订阅、撕裂和高频更新的一大类 bug。

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

用于:

  • 浏览器 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 回退方案
  );
}

使用 transitions 和 deferred values 实现更流畅的 UI

如果当用户输入或过滤时你的应用感觉迟钝,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) → 延迟 派生值

在需要时将它们一起使用,但不要过度。这些不是用于微不足道的计算。

可测试且可调试的 Hooks

现代 React DevTools 让检查自定义 Hooks 变得异常简单。如果你很好地构建你的 Hooks,大部分逻辑变得可以在不渲染实际组件的情况下进行测试。

  • 将领域逻辑与 UI 分离
  • 尽可能直接测试 Hooks
  • 将 provider 逻辑提取到自己的 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);
}

下次你必须调试它时,你会感谢自己。

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

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

值得了解的 API:

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

方向很明确:React 希望我们减少对”瑞士军刀”式 useEffect 使用的依赖,更多地依赖清晰的渲染驱动的数据流。

围绕派生状态和服务端/客户端边界设计你的 Hooks,可以让你的应用自然地面向未来。

Hooks 作为架构,而非语法

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

  • 保持派生状态在渲染中
  • 仅将 effects 用于实际的副作用
  • 通过小型、专注的 Hooks 组合逻辑
  • 让并发工具平滑异步流程
  • 跨越客户端和服务端边界进行思考

React 正在演进。我们的 Hooks 也应该随之演进。

如果你仍然像 2020 年那样编写 Hooks,那也没关系。我们大多数人都是如此。但 React 18+ 为我们提供了更好的工具箱,熟悉这些模式会很快得到回报。

本文作者 Matt Smith,吴文俊翻译,转载请注明来源链接:

原文链接: https://allthingssmitty.com/2025/12/01/react-has-changed-your-hooks-should-too/

本文链接: https://tie.pub/blog/react-changed-hooks-should-too/