《你不知道的React》-useSyncExternalStore

1,976 阅读3分钟

小而美的系列教程,周末定时更新,后续会推出基于 React 从 0~1 开发的三方插件,如果有收获,记得给个点赞。

相信很多同学都没使用过这个 API,官方描述为useSyncExternalStore is a React Hook that lets you subscribe to an external store,但如果你想开发一个 React 插件,那么它非常重要,由于这个 API 相对大多同学来说比较陌生,我们从以下两个方向阐述:

  1. 简介
  2. 使用案例
  3. 源码解读
  4. 总结

简介

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

useSyncExternalStore 是一个用于从外部数据源读取和订阅的 Hook,这个 Hooks 返回 store 的值并接受三个参数:

export function useSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T,
): T

subscribe: 注册回调的函数,返回一个() => void 用于清除副作用函数,每当 store 更改时调用该回调函数触发组件更新

getSnapshot:返回对应(想要)的 store,在 store 不变的情况下,如果重复调用 getSnapshot 必须返回相同值,否则会触发更新,需要避免类似 { ...store } 这类问题

getServerSnapshot:返回服务器渲染期间使用的快照的函数,一般般用于 SSR 场景

使用案例

监听网络状态:如果我们想监听网络状态,并封装一个 hook,正常情况下官方案例:

function useOnlineStatus() {
  // Not ideal: Manual store subscription in an Effect
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener("online", updateState);
    window.addEventListener("offline", updateState);
    return () => {
      window.removeEventListener("online", updateState);
      window.removeEventListener("offline", updateState);
    };
  }, []);
  return isOnline;
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

官方使用 useSyncExternalStore 的案例:

export function useOnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return isOnline;
}

function getSnapshot() {
  return navigator.onLine;
}

function subscribe(callback) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

是不是有种错觉“两种并无多大差别”?如果你只是简单看代码的话,确实没有多大差别。

最大的差别在于subscribe to an external store,前者必须依赖 setIsOnline 才能更新组件,也就是 useState;而后者不需要,我们可以做到数据与、逻辑的分离。

getSnapshot 仅负责返回我们需要使用到的 data,subscribe 用于监听 store 的变化(网络),同时调用 callback 触发组件更新。

相比前者,我们可以通过任意方式去改变 store,只要确保数据更新时能够触发 callback 的调用即可,非常利于三方库的开发,eg: React Query


案例2:为了说明 external store,我们再看一个例子,用户在输入框输入内容时,我们将对应的数据赋值给外部的 store,在 subscribe 里面监听 blur 事件,当 blur 调用 callback,触发组件更新。

const store = {
  words: "",
};

const getSnapshot = () => {
  return store.words;
};

const App = () => {
  const ref = useRef<HTMLInputElement>(null);
  
  const subscribe = (callback: () => void) => {
    ref.current?.addEventListener("blur", callback);
    return () => {
      ref.current?.removeEventListener("blur", callback);
    };
  };
  const words = useSyncExternalStore(subscribe, getSnapshot);

  return (
    <>
      <p>words: {words}</p>
      <input
        type="text"
        ref={ref}
        onChange={(e) => {
          store.words = e.target.value;
        }}
      />
    </>
  );
};

export default App;

p3.gif

源码解读

前者通过 useState 触发组件更新,后者又是如何触发组件更新的呢?

其实还是 useState ,只不过做了一层封装,源码地址:github.com/facebook/re…

简化后的代码如下:

export function useSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T,
) {
  const value = getSnapshot();
  const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});
  useLayoutEffect(() => {
    inst.value = value;
    inst.getSnapshot = getSnapshot;
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({inst});
    }
  }, [subscribe, value, getSnapshot]);

  useEffect(() => {
    // Check for changes right before subscribing. Subsequent changes will be
    // detected in the subscription handler.
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({inst});
    }
    const handleStoreChange = () => {
      if (checkIfSnapshotChanged(inst)) {
        // Force a re-render.
        forceUpdate({inst});
      }
    };
    // Subscribe to the store and return a clean-up function.
    return subscribe(handleStoreChange);
  }, [subscribe]);

  return value;
}

function checkIfSnapshotChanged<T>(inst: {
  value: T,
  getSnapshot: () => T,
}): boolean {
  const latestGetSnapshot = inst.getSnapshot;
  const prevValue = inst.value;
  try {
    const nextValue = latestGetSnapshot();
    return !is(prevValue, nextValue);
  } catch (error) {
    return true;
  }
}

源码实现非常简单,首先是调用 getSnapshot 初始化 value,并将 value 和 getSnapshot 通过 state 维护起来

其次是监听 subscribe, value, getSnapshot 的变化,如果有变化,直接更新

最后是监听 subscribe ,返回 subscribe 的调用,并将 handleStoreChange 作为形参传入,对应 callback,当监听到 online 状态时,触发 handleStoreChange 的调用,从而触发 forceUpdate

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

总结

useSyncExternalStore 相比其他 API,理解成本会高一点,但它的设计思想非常好,值得研究。