聊聊 React 中被低估的 useSyncExternalStore Hooks

简介: 聊聊 React 中被低估的 useSyncExternalStore Hooks

在 React 18 中新增加了很多 Hooks,其中包括 useSyncExternalStore(),它的作用是获取外部数据源。

在一些状态管理库中,这个 Hooks 已经被广泛才用了。比如 Redux 内部就在使用它来实现选择器系统。

那么我们如何在自己的代码中使用 useSyncExternalStore 呢?

本文会演示一个例子,在这个例子中,Hooks 会触发无用的渲染。然后我会再通过 useSyncExternalStore 来避免这种无用渲染。


Hooks 导致无用渲染


假设我使用了 React-Router 来开发应用,其中会用到 useLocation() 这个 Hook。

useLocation 会返回一个包含很多属性的对象,比如 pathname, hash, search 等。我们可能不会使用它的所有属性。但是当这些属性中的任意一个被更新时,只要使用该 Hooks 的组件就会重新渲染。

示例代码如下:


function CurrentPathname() {
  const { pathname } = useLocation();
  return <div>{pathname}</div>;
}
function CurrentHash() {
  const { hash } = useLocation();
  return <div>{hash}</div>;
}
function Links() {
  return (
    <div>
      <Link to="#link1">#link1</Link>
      <Link to="#link2">#link2</Link>
      <Link to="#link3">#link3</Link>
    </div>
  );
}
function App() {
  return (
    <div>
      <CurrentPathname />
      <CurrentHash />
      <Links />
    </div>
  );
}

当我们点击任何一个 link 标签时,hash 都会发生变化,同时 CurrentPathname 组件都会重新渲染,即使它甚至没有使用 hash 属性。

这个现象背后的道理是:当一个 Hooks 返回的数据我们并没有用到时,React 组件仍然会重新渲染。

如果你不注意,将 useLocation 放在 React 组件树的顶层使用,那么组件树中任意一个组件修改了 location 上面的属性,都可能会重新渲染整个组件数,对应用的性能损害极大。

拿 useLocation 举例的目的不是说 React-Router 做得不好,而是想说明这个问题。

尽管你现在知道了 Hooks 过度返回属性的危害,但是仍然很难保证自己写 Hooks 的时候为了便捷性而不会这样做,或者其他第三方 Hooks 库也可能过度返回属性。


useSyncExternalStore 能否破解?


React 官方文档中介绍了 useSyncExternalStore 的作用及用法:

useSyncExternalStore 是一个推荐用于从外部数据源读取和订阅的 Hooks,它与选择性水合和时间切片等并发渲染功能兼容。这个 Hooks 返回 store 的值并接受三个参数:

  • subscribe: 注册回调的函数,每当 store 更改时调用该回调函数。
  • getSnapshot:返回 store 当前值的函数。
  • getServerSnapshot:返回服务器渲染期间使用的快照的函数。


function useSyncExternalStore<Snapshot>(
  subscribe: (onStoreChange: () => void) => () => void,
  getSnapshot: () => Snapshot,
  getServerSnapshot?: () => Snapshot
): Snapshot;

从描述来看,这似乎有点抽象。我相信你也没有一下子能够明白它的作用。

React 提供了一个 beta 文档页面,其中给出了一个很好的例子:


function subscribe(callback) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}
function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true
  );
}
function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

有了示例代码,我们应该很容易明白了这个 Hooks 的作用了。


开发 useHistorySelector


现在我们利用 useSyncExternal 优化一下 useLocation。

浏览器的 history 也可以被视为外部数据源。

React-Router 暴露了 useSyncExternalStore 需要连接的所有属性:

案例使用 React-Router v5:React-Router v6 的解决方案将有所不同。

实现 useHistorySelector() 其实非常简单:


function useHistorySelector(selector) {
  const history = useHistory();
  return useSyncExternalStore(history.listen, () =>
    selector(history)
  );
}

然后使用这个 Hooks 重构我们的应用。


function CurrentPathname() {
  const pathname = useHistorySelector(
    (history) => history.location.pathname
  );
  return <div>{pathname}</div>;
}
function CurrentHash() {
  const hash = useHistorySelector(
    (history) => history.location.hash
  );
  return <div>{hash}</div>;
}

现在我们点击上面的 link 时,CurrentPathname 组件将不会重新渲染!


另一个例子:scrollY


我们可以订阅很多外部数据源,在上面实现自己的选择器系统。这样可以最大程度上优化 React 的重新渲染。

假设我们要使用 scrollY 来获取页面的位置。我们可以实现这个自定义的 Hooks:


function subscribe(onStoreChange) {
  global.window?.addEventListener("scroll", onStoreChange);
  return () =>
    global.window?.removeEventListener(
      "scroll",
      onStoreChange
    );
}
function useScrollY(selector = (id) => id) {
  return useSyncExternalStore(
    subscribe,
    () => selector(global.window?.scrollY),
    () => undefined
  );
}

现在可以把这个 Hooks 和选择器一起使用:


function ScrollY() {
  const scrollY = useScrollY();
  return <div>{scrollY}</div>;
}
function ScrollYFloored() {
  const to = 100;
  const scrollYFloored = useScrollY((y) =>
    y ? Math.floor(y / to) * to : undefined
  );
  return <div>{scrollYFloored}</div>;
}

当我们滚动页面时,ScrollYFloored 组件会比 ScrollY 组件重新渲染的次数更少!


总结


我个人感觉 useSyncExternalStore 这个 Hooks 目前在 React 生态系统中没有被充分使用,但它值得更多关注。我们完全可以订阅许多外部的数据源来改善应用性能。

如果你还没有升级到 React 18,npm 上有一个 shim:use-sync-external-store。你可以在旧版本的 React 中使用它。

\



相关文章
|
2月前
|
前端开发 JavaScript 开发者
深入理解React Hooks:提升前端开发效率的关键
【10月更文挑战第5天】深入理解React Hooks:提升前端开发效率的关键
|
2月前
|
前端开发 JavaScript
React Hooks 全面解析
【10月更文挑战第11天】React Hooks 是 React 16.8 引入的新特性,允许在函数组件中使用状态和其他 React 特性,简化了状态管理和生命周期管理。本文从基础概念入手,详细介绍了 `useState` 和 `useEffect` 的用法,探讨了常见问题和易错点,并提供了代码示例。通过学习本文,你将更好地理解和使用 Hooks,提升开发效率。
78 4
|
2月前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
2月前
|
前端开发 JavaScript API
探索React Hooks:前端开发的革命性工具
【10月更文挑战第5天】探索React Hooks:前端开发的革命性工具
|
1月前
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
1月前
|
前端开发
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
23 3
|
1月前
|
前端开发 JavaScript
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
|
1月前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
36 2
|
2月前
|
前端开发 开发者
React 提供的其他重要 Hooks
【10月更文挑战第20天】React 提供了一系列强大的 Hooks,除了 `useRef` 之外,还有许多其他重要的 Hooks,它们共同构成了函数式组件开发的基础。
40 6
|
26天前
|
前端开发 JavaScript
React Hooks 深入解析
React Hooks 深入解析
23 0