使用全局变量和 Hooks 在 React 中进行全局状态管理。状态管理其实并不难。
介绍
那么,有什么新情况呢?
请把代码给我看看。
如果组件需要共享状态怎么办?
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
介绍
首先,我想简单谈谈 React 中的状态管理。React 中的状态管理可以分为两部分。
- 地方政府管理
- 全球状态管理
当我们处理两个或多个组件之间不共享的状态时(即它们在单个组件内部使用),会使用本地状态。
当组件需要共享状态时,会使用全局状态。
React 提供了一种非常优秀且简单的方法来管理局部状态(React Hooks),但说到全局状态管理,可用的选项却令人眼花缭乱。React 本身提供了 Context API,许多用于管理全局状态的第三方库都基于此构建,但即便如此,这些 API 仍然不如 React 状态 Hooks 那样简洁直观。更不用说使用 Context API 管理全局状态的缺点了,本文暂不讨论这些缺点,但有很多文章对此进行了深入探讨,如果您想了解更多,可以查阅相关资料。
那么,有什么新情况呢?
今天我想介绍一种在 React 中管理全局状态的不同方法,我认为这种方法可以让我们构建一个像 hooks API 一样简单直观的全局状态管理 API。
状态管理的概念源于变量的概念,而变量是所有编程语言中最基础的概念之一。在状态管理中,我们有局部状态和全局状态,这分别对应于变量概念中的局部变量和全局变量。在这两种概念中,全局状态(或变量)的目的是允许在函数、类、模块、组件等实体之间共享,而局部状态(或变量)的目的是将其使用限制在声明它的作用域内,该作用域也可以是函数、类、模块、组件等。
这两个概念有很多共同之处,这让我不禁思考
:“如果我们能在 React 中使用全局变量来存储全局状态会怎么样?”。
于是,我决定进行一番实验。
请把代码给我看看。
我首先写了一个非常简单,可能也很愚蠢的例子,如下所示。
import React from 'react';
// use global variable to store global state
let count = 0;
function Counter(props){
let incrementCount = (e) => {
++count;
console.log(count);
}
return (
<div>
Count: {count}
<br/>
<button onClick={incrementCount}>Click</button>
</div>
);
}
ReactDOM.render(<Counter/>, document.querySelector("#root"));
正如你可能已经猜到的,这个例子可以渲染,count: 0但是如果你点击递增按钮,渲染后的值count并没有改变,而控制台输出的值却改变了。那么,为什么只有一个count变量却会出现这种情况呢?
这是因为当我们点击时,值会count递增(这就是为什么它会在控制台上打印递增的值),但组件Counter不会重新渲染以获取最新值count。
所以,这就是我们目前缺少的,才能使用全局变量count来存储全局状态。让我们尝试通过在更新全局变量时重新渲染组件来解决这个问题。这里我们将使用useState一个钩子来强制组件重新渲染,以便它能够获取新的值。
import React from 'react';
// use global variable to store global state
let count = 0;
function Counter(props){
const [,setState] = useState();
let incrementCount = (e) => {
++count;
console.log(count);
// Force component to re-render after incrementing `count`
// This is hack but bare with me for now
setState({});
}
return (
<div>
Count: {count}
<br/>
<button onClick={incrementCount}>Click</button>
</div>
);
}
ReactDOM.render(<Counter/>, document.querySelector("#root"));
这样就行了,每次点击都会重新渲染。
我知道,我知道这不是在 React 中更新组件的好方法,但请先别着急。我们只是想用全局变量来存储全局状态,而且它居然奏效了,所以我们暂时先这样吧。
好了,我们继续……
如果组件需要共享状态怎么办?
我们先来看一下全局状态的用途,
“当组件需要共享状态时,会使用全局状态”。
在之前的例子中,我们只在一个组件中使用了count全局状态,现在如果我们有第二个组件,也想在其中使用count全局状态,该怎么办呢?
好,我们来试试。
import React from 'react';
// use global variable to store global state
let count = 0;
function Counter1(props){
const [,setState] = useState();
let incrementCount = (e) => {
++count;
setState({});
}
return (
<div>
Count: {count}
<br/>
<button onClick={incrementCount}>Click</button>
</div>
);
}
function Counter2(props){
const [,setState] = useState();
let incrementCount = (e) => {
++count;
setState({});
}
return (
<div>
Count: {count}
<br/>
<button onClick={incrementCount}>Click</button>
</div>
);
}
function Counters(props){
return (
<>
<Counter1/>
<Counter2/>
</>
);
}
ReactDOM.render(<Counters/>, document.querySelector("#root"));
这里有两个组件Counter1A 和 B Counter2,它们都使用了counter全局状态。但是,当你点击 A 上的按钮时,它只会更新A 上Counter1的值。B 上的值将保持为 0。现在,当你点击 B 上的按钮时,它会更新,但会从 0 跳到 A 的最后一个值加 1。如果你返回到 A ,它也会发生同样的情况,从上次结束的位置跳到最后一个值加 1。countCounter1counter2Counter2Counter1Counter1Counter2
嗯……这很奇怪,可能是什么原因造成的呢?
原因在于,当你点击按钮时,Counter1它会递增值count,但它只会重新渲染Counter1,因为Counter1和Counter2没有共享重新渲染的方法,每个都有自己的incrementCount方法,当点击其中的按钮时,该方法会运行。
现在,当你点击它时,它会运行Counter2一个程序,该程序会获取一个已经递增的incrementCount值,并将其再次递增,然后重新渲染,这就是为什么计数的值会跳到上次的值加一的原因。如果你返回,同样的事情也会发生。countCounter1Counter1Counter1
所以问题在于,当一个组件更新全局状态时,其他共享该全局状态的组件并不知道,只有更新全局状态的组件才知道。因此,当全局状态更新时,其他共享该全局状态的组件不会重新渲染。
那么我们该如何解决这个问题呢?
乍一看似乎不可能,但如果你仔细观察,就会发现一个非常简单的解决方法。
由于全局状态是共享的,因此解决此问题的办法是让全局状态通知所有(共享它的)组件它已更新,以便它们都需要重新渲染。
但是,为了让全局状态通知所有使用它(订阅它)的组件,它必须首先跟踪所有这些组件。
为了简化流程,具体步骤如下
-
创建全局状态(严格来说,它是一个全局变量)
-
将一个或多个组件订阅到已创建的全局状态(这样全局状态就可以跟踪所有订阅它的组件)。
-
如果组件想要更新全局状态,它会发送更新请求。
-
当全局状态收到更新请求时,它会执行更新并通知所有订阅该状态的组件进行更新(重新渲染)。
你可能已经熟悉这种设计模式了,它非常流行,叫做观察者设计模式。
有了这些以及钩子函数的帮助,我们将能够使用全局变量完全管理全局状态。
我们先来实现全局状态。
function GlobalState(initialValue) {
this.value = initialValue; // Actual value of a global state
this.subscribers = []; // List of subscribers
this.getValue = function () {
// Get the actual value of a global state
return this.value;
}
this.setValue = function (newState) {
// This is a method for updating a global state
if (this.getValue() === newState) {
// No new update
return
}
this.value = newState; // Update global state value
this.subscribers.forEach(subscriber => {
// Notify subscribers that the global state has changed
subscriber(this.value);
});
}
this.subscribe = function (itemToSubscribe) {
// This is a function for subscribing to a global state
if (this.subscribers.indexOf(itemToSubscribe) > -1) {
// Already subsribed
return
}
// Subscribe a component
this.subscribers.push(itemToSubscribe);
}
this.unsubscribe = function (itemToUnsubscribe) {
// This is a function for unsubscribing from a global state
this.subscribers = this.subscribers.filter(
subscriber => subscriber !== itemToUnsubscribe
);
}
}
根据上述实现,今后创建全局状态的方式如下所示。
const count = new GlobalState(0);
// Where 0 is the initial value
至此,全局状态的实现就完成了,总结一下我们所做的工作:GlobalState
-
我们创建了一种通过订阅和取消订阅全局状态的
subscribe机制unsubscribe。 -
setValue我们创建了一种机制,当全局状态更新时,通过方法通知订阅者。 -
getValue我们创建了一种通过方法获取全局状态值的机制。
现在我们需要实现一种机制,允许我们的组件订阅、取消订阅并从中获取当前值GlobalState。
如前所述,我们希望我们的 API 像 Hooks API 一样简单易用、直观明了。因此,我们将创建一个useState类似 Hook 的全局状态管理 Hook。
我们将称之为useGlobalState……
它的用法将如下:
const [state, setState] = useGlobalState(globalState);
现在我们来写吧……
import { useState, useEffect } from 'react';
function useGlobalState(globalState) {
const [, setState] = useState();
const state = globalState.getValue();
function reRender(newState) {
// This will be called when the global state changes
setState({});
}
useEffect(() => {
// Subscribe to a global state when a component mounts
globalState.subscribe(reRender);
return () => {
// Unsubscribe from a global state when a component unmounts
globalState.unsubscribe(reRender);
}
})
function setState(newState) {
// Send update request to the global state and let it
// update itself
globalState.setValue(newState);
}
return [State, setState];
}
这就是我们的钩子函数正常工作所需的全部内容。钩子函数最重要的部分useGlobalState是订阅和取消订阅全局状态。请注意,useEffect钩子函数用于确保我们通过取消订阅全局状态来清理工作,以防止全局状态跟踪已卸载的组件。
现在让我们用钩子函数重写一下两个计数器的示例。
import React from 'react';
// using our `GlobalState`
let globalCount = new GlobalState(0);
function Counter1(props){
// using our `useGlobalState` hook
const [count, setCount] = useGlobalState(globalCount);
let incrementCount = (e) => {
setCount(count + 1)
}
return (
<div>
Count: {count}
<br/>
<button onClick={incrementCount}>Click</button>
</div>
);
}
function Counter2(props){
// using our `useGlobalState` hook
const [count, setCount] = useGlobalState(globalCount);
let incrementCount = (e) => {
setCount(count + 1)
}
return (
<div>
Count: {count}
<br/>
<button onClick={incrementCount}>Click</button>
</div>
);
}
function Counters(props){
return (
<>
<Counter1/>
<Counter2/>
</>
);
}
ReactDOM.render(<Counters/>, document.querySelector("#root"));
你会发现这个例子运行得非常完美。Counter1更新Counter2也能同步更新,反之亦然。
这意味着可以使用全局变量来管理全局状态。正如您所见,我们创建了一个非常简单易用且直观的全局状态管理 API,就像 hooks API 一样。我们完全避免了使用 Context API,因此无需 Provider 或 Consumer。
利用这种方法可以做很多事情,例如选择/订阅深度嵌套的全局状态、将全局状态持久化到本地存储、实现基于键的 API 来管理全局状态、实现useReducer类似全局状态的功能等等。
我本人用这种方法编写了一个完整的库来管理全局状态,它包含了所有提到的功能,如果你想查看的话,这是链接:https://github.com/yezyilomo/state-pool。
感谢您提出这一点,我想听听您的意见,您对这种方法有何看法?
文章来源:https://dev.to/yezyilomo/global-state-management-in-react-with-global-variables-and-hooks-state-management-doesn-t-have-to-be-so-hard-2n2c
