80 行代码实现全局 hook store

React hook 帮助我们很方便管理我们的组件的状态,但是组件中的 hook 状态是局部的,如果我们想要使用全局的 hook ,需要通过 Context 进行管理。

但是 Context 本身存在一些缺陷:

  • 使用略显繁琐,需要定义很多 Context.Provide 管理复杂状态
  • 没有提供selector 筛选context中的 state ,从而达到更细颗粒度的更新

而其他的全局状态库也并没有提供便利的 hook 的写法。所有尝试写一个自己的全局 hook store 并避免 Context 本身的问题。

实现思路

首先,我们思考一个问题,如果想要用 hook 去管理状态,就需要在 React 的组件中管理 hook 状态,受限于 Context 本身的一些局限性,所以我们需要实现自定义的 HookStore 组件去管理全局的状态,再建立和使用全局状态的组件的关联,当 hook 状态变化时通知使用的组件更新。而建立 HookStore 组件和使用 hook 的组件的关联,可以通过创建 中间层 store 去串联他们的关系。

store 初始化

首先我们需要创建 store,然后在 React 中自定义 HookStore 组件在 render 的过程中给 store 提供 state,这样子组件在 render 的时候就能获取到 store 中 state。

createStore

store 用来管理 state,这里 state 是 hook state 并不能直接在 store 中定义,需要在 React 组件 render 时提供。所以先在 store 中定义 createState 函数,在 自定义 HookStore 组件 render 的时候执行 createState 生成 hook state。

javascript 复制代码
let index = 0;
function createStoreImpl(createState) {
  const listener = createListener();
  const store = {
    key: index++,
    state: null,
    getState: () => store.state,
    setState: (state) => {
      store.state = state;
      listener.run(state);
    },
    createState,
    subscribe: listener.subscribe,
  };
  return store;
}

lisenter 是用来订阅 store 中的 state 变化的事件,在我们的状态库中可以订阅使用 store 组件的更新。

javascript 复制代码
function createListener() {
  const listeners = new Set();
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  const run = (state) => {
    listeners.forEach((listener) => {
      listener(state);
    });
  };
  return {
    subscribe,
    run,
  };
}
store 连接 HookStore 组件

怎么使 store 可以接入 React 组件中呢。

首先我们需要在 React 组件渲染前,准备好所有的 store。

javascript 复制代码
    export const stores = new Set();

    export const createStore = (createState) => {
      const store = createStoreImpl(createState);
      // 存储所有stores
      stores.add(store);
      ...
    };

    // 使用方式
    function useUser() {
      const [user, setUser] = useState('');
      return {
        user,
        setUser,
      };
    }
    createStore(useUser);

在 React 组件渲染前 createStore 创建 store,将所有的 store 存储到 stores 中。然后我们可以 在 React 中定义 HookStore 组件,在 HookStore 组件中拿到 store,在 HookStore 组件 render 时执行 createState 生成 hook state,将 hook state 添加到 store 中。因为我们有多个 store,并且每个 store 之间是隔离的,所以每个 store 都需要对应一个 HookStore 组件。

Prvoider 组件根据 stores 渲染多个 HookStore 组件

javascript 复制代码
export default function Prvoider({ children }) {
  return (
    <>
      {Array.from(stores).map((store) => (
        <HookStore store={store} key={store.key} />
      ))}
      {children}
    </>
  );
}

HookStore 组件中 store.createState() 生成 hook state,并通过 store.setState(state) 给 store 设置 state

javascript 复制代码
export default function HookStore({ store }) {
  const mounted = useRef(false);
  const state = store.createState();
  if (!mounted.current) {
    store.setState(state);
  }
  useEffect(() => {
    if (mounted.current) {
      store.setState(state);
    } else {
      mounted.current = true;
    }
  }, [state]);
  return null;
}

HookStore 组件 render 完 store 中已经存在 hook state 了。

组件中使用 store

在组件中怎么使用 store 呢。在 createStore 的时候,我们返回一个 useStore 方法,通过 useStore 我们可以很方便使用 store 中的状态。并且提供 selector 方式筛选 state,并且可以更细颗粒度的通过 selector 的 state 更新组件。

javascript 复制代码
export const createStore = (createState) => {
  const store = createStoreImpl(createState);
  stores.add(store);
  return (selector = (state) => state) => selector(store.state);
};

store 更新

在组件中触发 store 的 setState,因为 setState 是在 HookStore 组件中提供的,所以只会触发 HookStore 组件的更新,但是 hook 组件的更新又如何触发使用了 store 的组件更新呢,所以我们可以在组件在使用 store 时,让 store 订阅组件更新,这样在 store 的 state 发生变化时就可以通知使用了 store 组件的更新。

通知组件更新

在组件中使用 createStore 返回的 useStore 方法,在 useStore 方法中我们可以使用 React 官方提供的useSyncExternalStoreWithSelector 的方法,让 store 订阅 组件的更新。

javascript 复制代码
export const createStore = (createState, isEqual) => {
  const store = createStoreImpl(createState);
  stores.add(store);
  return (selector = (state) => state) =>
    useSyncExternalStoreWithSelector(
      store.subscribe,
      store.getState,
      store.getServerState || store.getState,
      selector,
      isEqual
    );
};

并且在useSyncExternalStoreWithSelector中,我们使用 selector 可以筛选 state,更细颗粒度的更新组件。

useSyncExternalStoreWithSelector

useSyncExternalStoreWithSelector 中 通过 useSyncExternalStore 实现对组件更新的订阅。在useSyncExternalStoreWithSelector中对 getSnapshot 的方法进行了扩展,让它具有selector state 的能力,并且可以使用isEqual自定义组件更新逻辑。

简化的useSyncExternalStoreWithSelector源码如下

javascript 复制代码
export function useSyncExternalStoreWithSelector<Snapshot, Selection>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => Snapshot,
  getServerSnapshot: void | null | (() => Snapshot),
  selector: (snapshot: Snapshot) => Selection,
  isEqual?: (a: Selection, b: Selection) => boolean
): Selection {
  const [getSelection] = useMemo(() => {
    const memoizedSelector = (nextSnapshot: Snapshot) => {
      const prevSnapshot: Snapshot = (memoizedSnapshot: any);
      const prevSelection: Selection = (memoizedSelection: any);
      if (is(prevSnapshot, nextSnapshot)) {
        return prevSelection;
      }
      const nextSelection = selector(nextSnapshot);
      if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) {
        return prevSelection;
      }
      memoizedSnapshot = nextSnapshot;
      memoizedSelection = nextSelection;
      return nextSelection;
    };
    const getSnapshotWithSelector = () => memoizedSelector(getSnapshot());
    return [getSnapshotWithSelector];
  }, [getSnapshot, selector, isEqual]);

  const value = useSyncExternalStore(
    subscribe,
    getSelection,
    getServerSelection
  );
  return value;
}

封装 getSelection 方式作为 useSyncExternalStore 的 getSnapshot。 在 getSelection 中通过 selector 过滤 state 中的值,并且通过isEqual比较新旧的 state 来决定是否返回新的 state,从而来决定在useSyncExternalStore接入时是否更新。

useSyncExternalStore

useSyncExternalStore 中通过提供的 subscribe 订阅组件的更新,而组件的更新判断是通过 getSnapshot 获取的 state 的新旧的值是否一致来决定的。

javascript 复制代码
// 简化 mountSyncExternalStore
function mountSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T,
): T {
  // 获取最新的状态
  const  nextSnapshot = getSnapshot();
  ......
  // 订阅更新
  subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]
  ......
  return nextSnapshot;
}

// 订阅更新
function subscribeToStore<T>(
  fiber: Fiber,
  inst: StoreInstance<T>,
  subscribe: (() => void) => () => void,
): any {
  const handleStoreChange = () => {
    // The store changed. Check if the snapshot changed since the last time we
    // read from the store.
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceStoreRerender(fiber);
    }
  };
  // Subscribe to the store and return a clean-up function.
  return subscribe(handleStoreChange);
}

//比较前后的状态
function checkIfSnapshotChanged<T>(inst: StoreInstance<T>): boolean {
  const latestGetSnapshot = inst.getSnapshot;
  const prevValue = inst.value;
  try {
    const nextValue = latestGetSnapshot();
    return !is(prevValue, nextValue);
  } catch (error) {
    return true;
  }
}

// react更新
function forceStoreRerender(fiber: Fiber) {
  const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
  if (root !== null) {
    scheduleUpdateOnFiber(root, fiber, SyncLane);
  }
}

总结

我们定义 HookState 组件管理全局 hook state。

创建 store 建立 HookState 和使用 store 组件的联系,当 hook state 变化时,通知使用 store 的组件的更新。

在 useStore 时通过useSyncExternalStoreWithSelector 订阅组件的更新,并通过selectorisEqual 实现 state 筛选,和更细颗粒度的组件更新。

npm 链接(react-global-hook-store)

github 链接(react-global-hook-store)

相关推荐
贵沫末8 分钟前
React——基础
前端·react.js·前端框架
aklry19 分钟前
uniapp三步完成一维码的生成
前端·vue.js
Rubin9327 分钟前
判断元素在可视区域?用于滚动加载,数据埋点等
前端
爱学习的茄子27 分钟前
AI驱动的单词学习应用:从图片识别到语音合成的完整实现
前端·深度学习·react.js
用户38022585982428 分钟前
使用three.js实现3D地球
前端·three.js
程序无bug30 分钟前
Spring 面向切面编程AOP 详细讲解
java·前端
zhanshuo30 分钟前
鸿蒙UI开发全解:JS与Java双引擎实战指南
前端·javascript·harmonyos
JohnYan30 分钟前
模板+数据的文档生成技术方案设计和实现
javascript·后端·架构
撰卢1 小时前
如何提高网站加载速度速度
前端·javascript·css·html
10年前端老司机1 小时前
在React项目中如何封装一个可扩展,复用性强的组件
前端·javascript·react.js