原文链接:useGlobalState()
演示:useGlobalState Hooks - CodeSandbox

这篇文章介绍了一种简单的方法来共享React状态,而不需要使用context或复杂库。作者提供了一个自定义挂钩,使用全局变量来存储数据和监听器。使用这个挂钩,组件之间可以共享一个状态。文章还讨论了常见的解决方案,如使用prop drillingcontext,以及这些方法存在的问题。最后,作者提供了一个包含示例的完整代码实现。

React中因为状态流向和作用域的问题,无法在组件之间共享状态,状态只能向下传递,即是使用了自定义的 Hooks,并且在多个组件之间使用该 Hooks,每个组件也将获得自己的状态实例。

Props 下钻

最简单的做法是使用**Props下钻**,也就是说将状态提升至最顶层的父级组件中,通过 Props 将状态传递给子组件,达到多个子组件共享状态的目的,这种方式存在一些问题

  1. 需要共享状态的组件必须拥有共同的父级
  2. 组件层级较深时,复杂度提高,加重心智负担
  3. 状态更新时,如果不处理好 memo,将会造成多余的重新渲染

Context

React 引入了 Context 概念,用来处理状态共享和Props下钻时层级过深的问题,对于基本的情况来说,但是当复杂度进一步提升,存在多个 Context 时,就会出现很多的嵌套上下文

<SharedStateContext1> 
	<SharedStateContext2> 
		<SharedStateContext3>
		</SharedStateContext2> 
	</SharedStateContext2>
</SharedStateContext1>

或许可以使用一个唯一的 Context ,但是这种方法存在的问题时,对于共享状态的任何更改,都会导致所有消费者的重新渲染。无法有选择的更新某些消费者,一旦更新了提供者的值,所有的消费者都会重新渲染,极大地影响性能。
另一个问题时,当共享状态需要变化时,必须更新提供者(公共父组件),不能只更新共享状态的子节点。如果子组件没有使用memo或者PureComponent 进行适当优化,则会存在性能问题。

解决方案

使用观察者模式可以很好的解决这个问题,首先需要在全局变量中存储SubjectObservers,调用钩子时注册一个新的监听器, 任何组件调用Hook 的状态更新时,向所有的监听器发出信号。
为了支持多个状态,需要使用一个唯一标识符来标识状态,这里建议使用Symbol,需要注意,唯一标识符不能再 useGlobalState内部定义,因为每次调用时都会创建一个新的表示符,需要在全局定义一次。

const store = {};
const listeners = {};
 
function useGlobalState(key, initialValue) {
    const [state, _setState] = useState(store[key] || initialValue);
    const setState = useCallback(stateOrSetter => {
        let next = stateOrSetter;
        if (typeof stateOrSetter === 'function') {
            next = stateOrSetter(store[key]);
        }
        listeners[key].forEach(l => l(next));
        store[key] = next;
    }, []);
 
    useEffect(() => {
        // Store the initial state on the first call with this key
        if (!store[key]) {
            store[key] = initialValue;
        }
        // Create an empty array of listener on the first call with this key
        if (!listeners[key]) {
            listeners[key] = [];
        }
        // Register the observer
        const listener = state => _setState(state);
        listeners[key].push(listener);
 
        // Cleanup when unmounting
        return () => {
            const index = listeners[key].indexOf(listener);
            listeners[key].splice(index, 1);
        };
    }, [])
 
    return [state, setState];
}

使用方式与useState一样

// 创建全局状态唯一标识符
const COUNT_KEY = Symbol();
 
// 使用
const [state, setState] = useGlobalState(COUNT_KET, 0);

生成器&优化唯一标识符

每次使用 Hook 时都需要手动创建 Symbol唯一标识符,比较麻烦,可以通过创建一个 生成器函数来淡化 Key 的概念,同时,将每个状态对应的监听器分开存放。

完整代码

import { useState, useCallback, useEffect } from "react";
 
export type StateType<T> = {
  listeners: Array<(state: T) => void>;
  state: T;
};
 
/**
 * 生成器函数,用于简化使用和实现
 */
export function createGlobalState<T>(initialValue: T): StateType<T> {
  return {
    listeners: [],
    state: initialValue,
  };
}
 
const useGlobalState = <T>(config: StateType<T>) => {
  const [state, _setState] = useState(config.state);
 
  const setState = useCallback((stateOrSetter: T | ((_state: T) => T)) => {
    config.listeners.forEach((l) =>
      l(
        stateOrSetter instanceof Function
          ? stateOrSetter(config.state)
          : stateOrSetter
      )
    );
    config.state =
      stateOrSetter instanceof Function
        ? stateOrSetter(config.state)
        : stateOrSetter;
  }, []);
 
  useEffect(() => {
    // 注册 observer
    const listener = (state: T) => _setState(state);
    config.listeners.push(listener);
 
    // 组件卸载时移除 observer
    return () => {
      const index = config.listeners.indexOf(listener);
      config.listeners.splice(index, 1);
    };
  }, []);
 
  return [state, setState] as const;
};
 
export default useGlobalState;
 

使用

创建组件

import useGlobalState, { createGlobalState } from "./useGlobalState";
 
const COUNT = createGlobalState(0);
 
export const ComponentA = () => {
  const [count, setCount] = useGlobalState(COUNT);
 
  return (
    <div onClick={() => setCount((state) => state + 1)}>
      ComponentA - {count}
    </div>
  );
};

在多个组件中使用同一个状态

function App() {
  return (
    <div className="App">
		<ComponentA />
		<ComponentA />
    </div>
  );
}
 
export default App;

配合 useSyncExternalStore 使用

useSyncExternalStore 是 React 18 的新钩子,可以在 React 内部和外部存储之间同步状态

export function createState(initialValue) {
    return {
        listeners: [],
        state: initialValue,
    };
}
 
export function useGlobalState(config) {
    const setState = useCallback(stateOrSetter => {
        let next = stateOrSetter;
        if (typeof stateOrSetter === 'function') {
            next = stateOrSetter(config.state);
        }
        config.state = next;
        config.listeners.forEach(l => l());
    }, []);
 
    const state = useSyncExternalStore(
        (listener) => {
            // Register the observer
            config.listeners.push(listener);
 
            // Cleanup when unmounting
            return () => config.listeners.filter(l => l !== listener);
        },
        () => config.state,
    );
    return [state, setState];
}