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)

相关推荐
dy171735 分钟前
element-plus表格默认展开有子的数据
前端·javascript·vue.js
2501_915918414 小时前
Web 前端可视化开发工具对比 低代码平台、可视化搭建工具、前端可视化编辑器与在线可视化开发环境的实战分析
前端·低代码·ios·小程序·uni-app·编辑器·iphone
程序员的世界你不懂5 小时前
【Flask】测试平台开发,新增说明书编写和展示功能 第二十三篇
java·前端·数据库
索迪迈科技5 小时前
网络请求库——Axios库深度解析
前端·网络·vue.js·北京百思可瑞教育·百思可瑞教育
gnip5 小时前
JavaScript二叉树相关概念
前端
一朵梨花压海棠go6 小时前
html+js实现表格本地筛选
开发语言·javascript·html·ecmascript
attitude.x6 小时前
PyTorch 动态图的灵活性与实用技巧
前端·人工智能·深度学习
β添砖java6 小时前
CSS3核心技术
前端·css·css3
空山新雨(大队长)6 小时前
HTML第八课:HTML4和HTML5的区别
前端·html·html5