🔥 useSyncExternalStore - 被你低估的 React API

651 阅读4分钟

✋🏻 基础使用

在这里我们先看看如何使用 useSyncExternalStore 这个 新的 API 。之后我们会尝试用这个 API 写一个简单的状态管理器。

当你看完了官方文档之后,让我们来写一个 useScrollY Hook,用于了解 useSyncExternalStore 的使用方法。那我们就先使用 useState、 useEffect 来写这个简单的例子,之后我们使用 useSyncExternalStore 进行替换。

例如👇 示例:

function useScrollY() {
  const [state, setState] = useState(0);

  const onScroll = () => {
    setState(window.scrollY);
  };

  useEffect(() => {
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  return state;
}

上面的例子十分的简单,我相信你已经看懂了代码,我们使用了 useState 、 useEffect 就实现了这个 Hook。但是如何使用 useSyncExternalStore 这个 API 呢?

请看 👇🏻 例子:

function subscribe(onStateChange) {
  window.addEventListener("scroll", onStateChange);
  return () => window.removeEventListener("scroll", onStateChange);
}

function useScrollY(selector) {
  return useSyncExternalStore(subscribe, () => selector(window.scrollY));
}

首先我们需要一个 subscribe 函数,这函数的主要作用就是用于注册一个回调函数,当存储值发生更改时被调用。接下来我们就需要返回当前的值的函数,也就是第二个参数了。

这样我们就使用 useSyncExternalStore 这个新的 API 实现了这个 Hook。 🔗 在线示例

✋🏻 全局状态管理

经过👆🏻简单的应用,接下来我们开始实现一个简单的状态管理工具。我们可以先使用 useState 、 useEffect 来实现一个简单的全局状态管理,也涉及到了一点点的优化知识,最后我们可以使用 useSyncExternalStore 这个新的 API 进行替换。这样也可以让你循序渐进的理解这个新的 API。

我们先创建一个 store.ts 文件,用于编写创建 Store 的代码。

基本的示例代码如下:

import { useEffect, useState } from "react";

function createStore(initStore: { count1: number; count2: number }) {
  let state = initStore;
  const listeners = new Set<
    (state: { count1: number; count2: number }) => void
  >();
  return {
    getState() {
      return state;
    },
    setState(newState: { count1: number; count2: number }) {
      state = newState;
      listeners.forEach((listener) => listener(newState));
    },
    subscribe(listener: (state: { count1: number; count2: number }) => void) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

const store = createStore({
  count1: 0,
  count2: 0,
});

const useStore = () => {
  const [state, setState] = useState(store.getState());
  useEffect(() => {
    store.subscribe(setState);
  }, []);
  return state;
};

上面的代码包含了 createStoreuseStore 方法,一个是创建 Store 的方法,后者则是使用 Store 的 Hook 。

接下来就是如何使用这个简单的状态管理。例如下面的代码:

// App.tsx
import { store, useStore } from "./store";

function DisplayCounter({ field }: { field: "count1" | "count2" }) {
  const counter = useStore();
  return <div>{counter[field]}</div>;
}

function HandleCounter({ field }: { field: "count1" | "count2" }) {
  const counter = useStore();
  return (
    <button
      onClick={() => {
        const count = counter[field] + 1;
        store.setState({ ...counter, [field]: count });
      }}
    >
      +{field}
    </button>
  );
}

function App() {
  return (
    <div className="App">
      <DisplayCounter field="count1" />
      <HandleCounter field="count1" />
      <br />
      <DisplayCounter field="count2" />
      <HandleCounter field="count2" />
    </div>
  );
}

这样我们就实现了一个简单的状态管理。

点击该示例查看详情 👉🏻 🔗 在线示例

但是这一版代码是存在重复渲染的问题的。例如下面的动图 👇🏻:

2022-10-15 10.36.12.gif 我们都知道 如何优化全局状态与组件内部状态之间的连接,对于提高 React 组件性能有着至关重要的作用。 因此这个重复渲染的问题我们必须要修复。

分析代码我们可以知道造成如上图的重复渲染的原因是,我们 store 中的 setState 方法,每次值的引用都在变动,所以会造成页面的重复渲染。

那我们需要怎么优化呢?很简单,我们只需要选择我们自己需要的值进行渲染就可以了,那应该怎么做呢?我们只需要给 useStore 这个 Hook 传递一个 selector 函数,选择我们需要的状态值就可以了。代码如下所示:

const useStore = (
  selector: (state: { count1: number; count2: number }) => number
) => {
  const [state, setState] = useState(selector(store.getState()));
  useEffect(() => {
    store.subscribe((state) => setState(selector(state)));
  }, []);
  return state;
};

改造完毕之后我们再修改使用的代码。例如:

function DisplayCounter({ field }: { field: "count1" | "count2" }) {
  const counter = useStore((store) => store[field]);
  return <div>{counter}</div>;
}

function HandleCounter({ field }: { field: "count1" | "count2" }) {
  const counter = useStore((store) => store[field]);
  return (
    <button
      onClick={() => {
        const count = counter + 1;
        store.setState({ ...store.getState(), [field]: count });
      }}
    >
      +{field}
    </button>
  );
}

点击示例查看详情 👉🏻🔗 在线示例

我们已经理解了这个全局状态管理的原理,接下来我们就看看如何使用 useSyncExternalStore 对其进行替换。其实和上面的 useScrollY Hook 一样,我们只需要改造 useStore 这个 Hook 就可以了。

例如 👇 的代码所示:

const useStore = (
  selector: (state: { count1: number; count2: number }) => number
) => {
  return useSyncExternalStore(store.subscribe, () =>
    selector(store.getState())
  );
};

点击示例查看详情 👉 🔗 在线示例

希望本文可以让你重视 useSyncExternalStore 这个 API。

如果您还有什么没有理解的地方可以在下方留言告诉我哦!😁!

👉👉👉👉👉 🔗 源码

👏 往期精彩