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

本文以计数器组件的开发为例子,介绍一种使用 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>
)
}

代码执行的效果如下所示:


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

计数器支持初始值

上面示例中的两个计数器之间是独立计数的,没有任何关联。接下来对 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>
);

代码执行的效果如下所示:


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


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

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>
);
}

代码执行的效果如下所示:

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


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




关注我们的公众号,阅读更多前端技术文章


原文始发于微信公众号(KooFE前端团队):React 实现全局状态管理的一种方案

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/54283.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!