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)

相关推荐
M_emory_13 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito16 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
abments1 小时前
JavaScript逆向爬虫教程-------基础篇之常用的编码与加密介绍(python和js实现)
javascript·爬虫·python
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
老码沉思录1 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript