本文以计数器组件的开发为例子,介绍一种使用 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];
};
对 Counter
和 APP
做相应的调整:
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前端团队」关注前端技术动态。