React 实现全局状态管理的一种方案

5,240 阅读5分钟

本文以计数器组件的开发为例子,介绍一种使用 React Hooks 实现全局状态管理的方法。

简单的计数器组件

在下面的代码定义了一个简单的计数器组件 Counter,从 0 开始计数,每点击一次计数器加一:

const Counter = ({ text }) => {
  const [count, setCount] = useState(0);
  const addCount = () => setCount(count + 1);

  return (
    <div onClick={addCount}>
      {text}: {count}
    </div>
  );
};

我们可以对上面的 Counter 做进一步抽象,用一个自定义的 Hooks useCounter 来实现计数逻辑:

const useCounter = (initCount) => {
  const [count, setCount] = useState(initCount || 0);
  const addCount = () => setCount(count + 1);

  return [count, addCount];
};

const Counter = ({ text }) => {
  // 使用自定义 Hooks useCounter
  const [count, addCount] = useCounter(0);
  return (
    <div onClick={addCount}>
      {text}: {count}
    </div>
  );
};

两段代码实现的计数器组件功能有任何区别,我们使用 Counter 组件来实现两个计数器:

const App = () => {
  return (
    <div>
      <Counter text="计数器1" />
      <Counter text="计数器2" />
    </div>
  )
}

代码示例如下:

计数器支持初始值

上面示例中的两个计数器之间是独立计数的,没有任何关联。接下来对 Counter 组件做一些改造,增加了一个 props 参数 initCount,可以通过 initCount 来设置计数器的初始值。

const useCounter = (initCount) => {
  const [count, setCount] = useState(initCount || 0);

  // 组件更新自己的计数,每次执行时 count 加 1
  const addCount = () => setCount(count + 1);

  // 拿外面的最新计数,props 中的 initCount 发生变化,则重置计数
  useEffect(() => {
    setCount(initCount);
  }, [initCount]);

  return [count, addCount];
};

const Counter = ({ initCount, text }) => {
  const [count, addCount] = useCounter(initCount);
  return (
    <div onClick={addCount}>
      {text}: {count}
    </div>
  );
};

const App = () => {
  return (
    <div>
      <Counter initCount={1} text="计数器1" />
      <Counter initCount={0} text="计数器2" />
    </div>
  )
}

代码示例如下:

计数器之间同步更新

如果要求两个计数器同步计数,也就是其中一个计数器被点击时,两个计数器的数字保持一致且同时加一,该如何实现呢?很明显在这种场景下,可以让两个计数器来共享父组件中计数状态,需要对组件做进一步调整,除了能够接收父组件的计数状态之外,还要能够接收父组件修改计数的方法。在这里增加了 onChange

const useCounter = (initCount, onChange) => {
  const [count, setCount] = useState(initCount || 0);
  
  // 组件更新自己的计数
  const addCount = onChange || (() => setCount(count + 1));

  // 拿外面的最新计数
  useEffect(() => {
    setCount(initCount);
  }, [initCount]);

  return [count, addCount];
};

const Counter = ({ initCount, onChange, text }) => {
  const [count, addCount] = useCounter(initCount, onChange);
  return (
    <div onClick={addCount}>
      {text}: {count}
    </div>
  );
};

App 中可以实现两个计数器组件来同时更新计数:

const App = () => {
  const [count, setCount] = useState(0);
  const addCount = () => setCount(count + 1);

  return (
    <div className="App">
      <Counter initCount={count} text="计数器1" onChange={addCount} />
      <Counter initCount={count} text="计数器2" onChange={addCount} />
    </div>
  );

上面这段代码可以进一步优化为:

const App = () => {
  const [count, addCount] = useCounter(0)

  return (
    <div className="App">
      <Counter initCount={count} text="计数器1" onChange={addCount} />
      <Counter initCount={count} text="计数器2" onChange={addCount} />
    </div>
  );

代码示例如下:

到这里,我们通过父子组件共享状态的方式,实现了组件间通信。接下来将通过一种简单的状态管理,来实现同样的功能。

计数器之间的状态管理

我们在 useCounter 基础上进一步做改造,那么要做哪些改造呢?

  • 首先,将 initCount 作为一个全局变量,这样每个计数器都使用它的值。
  • 然后,当组件更新自己的计数时,需要更新全局的 initCount
  • 最后,当 initCount 发生变化时,各个计数器要拿到别人最新计数
// useCounter 代码
const useCounter = (initCount, onChange) => {
  const [count, setCount] = useState(initCount || 0);
  
  // 组件更新自己的计数
  const addCount = onChange || (() => setCount(count + 1));

  // 拿外面的最新计数
  useEffect(() => {
    setCount(initCount);
  }, [initCount]);

  return [count, addCount];
};

这里将 useCounter 进行改写为 useGlobalCounter

let initCount = 0;

const useGlobalCounter = () => {
  const [count, setCount] = useState(initCount || 0);
  const addCount = () => {
    // 更新 initCount
    initCount += 1;

    // 告知大家 initCount 发生变化,让大家更新计数
    ...
  }

  useEffect(() => {
    // 拿 initCount 的最新计数更新状态
    setCount(initCount);
    ...
  }, []);

  return [count, addCount];
};

很显然,我们需要在 addCount 中更新 initCount, 并且要让其他的计数器感知到数据发生变化,其他计数器一旦感知到变化后要渲染最新的计数。这是一种典型的发布订阅场景,addCount 中发布数据更新的消息,在 useEffect 中订阅数据的变化。于是上面的代码可以进一步完善:

let initCount = 0;
const listeners = new Set();

const useGlobalCounter = () => {
  const [count, setCount] = useState(initCount || 0);
  const addCount = () => {
    // 更新 initCount
    initCount += 1;

    // 告知大家 initCount 发生变化,让大家更新计数
    listeners.forEach((listener) => listener());
  }

  useEffect(() => {
    const listener = () => {
      // 拿 initCount 的最新计数更新状态
      setCount(initCount);
    };

    // 在 initCount 更新时调用 listeners 中每个 listener
    listeners.add(listener);
    
    // 避免在 add 之前 initCount 已经发生变化
    listener();

    return () => {
      listeners.delete(listener);
    };
  }, []);

  return [count, addCount];
};

CounterAPP 做相应的调整:

const Counter = ({ initCount, text }) => {
  const [count, addCount] = useGlobalCounter(initCount);
  return (
    <div onClick={addCount}>
      {text}: {count}
    </div>
  );
};

const App = () => {
  return (
    <div>
      <Counter text="计数器1" />
      <Counter text="计数器2" />
    </div>
  )
}

代码示例如下:

这样,就实现了两个计数器同步更新的功能,与父子组件之间的状态同步效果类似。但是这个实现里面有个弊端,只限于计数器之间共享状态,那么如何做得更通用一些呢?

更通用的全局状态管理

接下来,对上面的代码进一步升级改造为 createGlobalState,就可以实现一个简单的全局状态管理工具:

const createGlobalState = (initialState) => {
  let globalState = initialState;
  const listeners = new Set();

  const setGlobalState = (nextGlobalState) => {
    globalState = nextGlobalState;
    listeners.forEach(listener => listener());
  };

  const useGlobalState = () => {
    const [state, setState] = useState(globalState);
    useEffect(() => {
      const listener = () => {
        setState(globalState);
      };
      listeners.add(listener);
      listener();
      return () => listeners.delete(listener);
    }, []);
    return [state, setGlobalState];
  };

  return {
    setGlobalState,
    useGlobalState,
  };
};

使用 createGlobalState 来进行状态管理:


const { useGlobalState } = createGlobalState(0);

const Counter = ({ text }) => {
  const [state, setGlobalState] = useGlobalState();
  return (
    <div onClick={() => setGlobalState(state + 1)}>
      {text}: {state}
    </div>
  );
};

const App = () => {
  const [state] = useGlobalState();
  return (
    <div className="App">
      <Counter text="计数器" />
      <div>点击次数:{state}</div>
    </div>
  );
}

代码示例如下:

这里 createGlobalState 实现相对比较简单,功能不够完善。如果感兴趣的话可以了解一下 react-hooks-global-state 这个状态管理工具,本文中介绍的实现思路来源于这个库。


微信搜索 ikoofe, 关注公众号「KooFE前端团队」关注前端技术动态。