你绝对可以使用全局变量来管理 React 中的全局状态
介绍
React 提供了一种非常好且简单的方法,通过状态钩子来管理本地状态,但是当涉及到全局状态时,可用的选项就太多了。
React 本身提供了 context API,许多用于管理全局状态的第三方库都是在它之上构建的,但是构建的 API 仍然不像状态钩子那样简单直观,更不用说使用 context API 管理全局状态的缺点了,我们不会在本文中讨论它,但有很多文章讨论它。
因此,在 React 中管理全局状态仍然是一个尚无明确解决方案的问题。
但是如果我告诉您可能有一个基于全局变量的解决方案呢?
是的,您每天在代码中使用的全局变量。
这怎么可能呢?
管理状态的概念与变量的概念非常相似,变量的概念在几乎所有编程语言中都非常基础。
在状态管理中,我们有局部状态和全局状态,它们对应于变量概念中的局部变量和全局变量。
在这两个概念中,全局(状态和变量)的目的是允许在实体之间共享值,这些实体可能是函数、类、模块、组件等,而本地(状态和变量)的目的是将其使用限制在声明的范围内,该范围也可能是函数、类、模块、组件等。
所以这两个概念有很多共同点,这让我问自己一个问题
“如果我们使用全局变量来存储 React 中的全局状态会怎么样?”
答案
到目前为止,我们可以使用普通的全局变量来存储全局状态,但是当我们想要更新它时就会出现问题。
如果我们使用常规全局变量来存储 React 全局状态,那么当状态更新时,我们将无法立即获取其最新值,因为 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
会增加(这就是它在控制台上打印增加的值的原因)但组件Counter
不会重新渲染以获取最新的值count
。
所以这是阻碍我们在 React 中使用全局变量来管理全局状态的唯一问题。
解决方案
由于全局状态在组件之间共享,我们问题的解决方案是让全局状态通知所有依赖它的组件它已被更新,以便所有组件重新渲染以获取新值。
但是为了让全局状态通知所有使用它的组件(订阅它),它必须首先跟踪这些组件。
为了简化,流程如下
-
创建一个全局状态(从技术上讲是一个全局变量)
-
将组件订阅到已创建的全局状态(这可以让全局状态跟踪所有订阅它的组件)
-
如果组件想要更新全局状态,它会发送更新请求
-
当全局状态收到更新请求时,它会执行更新并通知所有订阅它的组件,以便它们自行更新(重新渲染)以获取新值
有了这个以及钩子的一点帮助,我们将能够使用全局变量完全管理全局状态。
幸运的是,我们不需要自己实施这一措施,因为有State Pool为我们提供支持。
介绍State Pool ✨🎉。
State Pool是一个基于全局变量和 React Hooks 的 React 状态管理库。它的 API 与 React Hooks 一样简洁直观,因此,如果您曾经使用过 React Hooks(useState
或useReducer
),那么您会对state-pool感到非常熟悉。您可以说state-pool是 React Hooks 的全局版本。
使用State Pool的特点和优势
- 简单、熟悉且极简的核心 API,但功能强大
- 内置状态持久性
- 非常容易学习,因为它的 API 与 React State Hook 的 API 非常相似
- 支持选择深度嵌套状态
- 支持动态创建全局状态
- 支持基于密钥和非基于密钥的全局状态
- 状态存储为全局变量(可在任何地方使用)
安装
yarn add state-pool
或者
npm install state-pool
入门
现在让我们看一个简单的例子,说明如何使用state-pool来管理全局状态
import React from 'react';
import {store, useGlobalState} from 'state-pool';
store.setState("count", 0);
function ClicksCounter(props){
const [count, setCount] = useGlobalState("count");
let incrementCount = (e) => {
setCount(count+1)
}
return (
<div>
Count: {count}
<br/>
<button onClick={incrementCount}>Click</button>
</div>
);
}
ReactDOM.render(ClicksCounter, document.querySelector("#root"));
如果你曾经使用过useState
React Hook,那么上面的例子应该非常熟悉,
让我们分解一下
-
在第二行,我们
store
从中useGlobalState
导入state-pool
。 -
我们将使用它
store
来保存我们的全局状态,因此store
它只是一个全局状态的容器。 -
我们还将使用
useGlobalState
全局状态来连接到我们的组件中。 -
第三行
store.setState("count", 0)
用于创建一个名为“count”的全局状态,并将 0 指定为其初始值。 -
第五行
const [count, setCount] = useGlobalState("count")
用于将名为“count”的全局状态(我们在第 3 行创建的)挂接到ClicksCounter
组件中。
正如您所见,它在很多方面都useGlobalState
非常相似。useState
更新嵌套全局状态
状态池提供了一种非常好的方式来处理全局状态更新,尤其是setState
在处理嵌套全局状态时。
让我们看一个嵌套全局状态的示例
import React from 'react';
import {store, useGlobalState} from 'state-pool';
store.setState("user", {name: "Yezy", age: 25});
function UserInfo(props){
const [user, setUser, updateUser] = useGlobalState("user");
let updateName = (e) => {
updateUser(function(user){
user.name = e.target.value;
});
}
return (
<div>
Name: {user.name}
<br/>
<input type="text" value={user.name} onChange={updateName}/>
</div>
);
}
ReactDOM.render(UserInfo, document.querySelector("#root"));
在这个例子中,一切都与上一个示例相同
在第三行,我们创建一个名为“用户”的全局状态并将{name: "Yezy", age: 25}
其设置为其初始值。
在第五行,我们使用useGlobalState
名为“用户”的全局状态(我们在第 3 行创建的)挂接到UserInfo
组件中。
然而这里我们除了返回一个函数之外还返回一个函数setUser
,updateUser
该函数用于更新用户对象而不是设置它,尽管您也可以使用它来设置用户对象。
因此这里updateUser
用于更新用户对象,它是一个高阶函数,它接受另一个用于更新用户的函数作为参数(另一个函数以用户(旧状态)作为参数)。
因此,要更新用户的任何嵌套值,您只需执行以下操作
updateUser(function(user){
user.name = "Yezy Ilomo";
user.age = 26;
})
您还可以返回新状态而不是更改它,即
updateUser(function(user){
return {
name: "Yezy Ilomo",
age: 26
}
})
因此返回的数组useGlobalState
是这种形式[state, setState, updateState]
state
保持全局状态的值setState
用于设置全局状态updateState
用于更新全局状态
选择嵌套状态
有时您可能有一个嵌套的全局状态,但某些组件需要使用其中的一部分(嵌套或派生值而不是整个全局状态)。
例如,在前面的例子中,我们有一个名为“用户”的全局状态,其值为,{name: "Yezy", age: 25}
但在组件中UserInfo
我们只使用/需要user.name
。
使用我们之前使用的方法,UserInfo
即使发生变化,组件也会重新渲染,user.age
这对性能不利。
状态池允许我们选择和订阅嵌套或派生状态,以避免依赖于全局状态的嵌套或派生部分的组件不必要地重新渲染。
下面是一个展示如何选择嵌套全局状态的示例。
import React from 'react';
import {store, useGlobalState} from 'state-pool';
store.setState("user", {name: "Yezy", age: 25});
function UserInfo(props){
const selector = (user) => user.name; // Subscribe to user.name only
const patcher = (user, name) => {user.name = name}; // Update user.name
const [name, setName] = useGlobalState("user", {selector: selector, patcher: patcher});
let handleNameChange = (e) => {
setName(e.target.value);
}
return (
<div>
Name: {name}
<br/>
<input type="text" value={name} onChange={handleNameChange}/>
</div>
);
}
ReactDOM.render(UserInfo, document.querySelector("#root"));
selector
到目前为止,从上面的例子来看,除了我们传递和patcher
钩住的部分之外,一切都应该是熟悉的useGlobalState
。
为了清楚起见,useGlobalState
接受第二个可选参数,即配置对象。selector
并且patcher
是可用配置之一。
-
selector
: 应该是一个函数,它接受一个全局状态作为参数,并返回一个选定的值。这样做的目的是订阅深度嵌套的状态。 -
patcher
: 应该是一个接受两个参数的函数,第一个参数是全局状态,第二个参数是所选值。这样做的目的是,一旦所选值更新,就将其合并回全局状态。
因此现在即使user.age
发生变化,组件UserInfo
也不会重新渲染,因为它只依赖于user.name
动态创建全局状态
状态池允许动态创建全局状态,如果全局状态的名称或值取决于组件内的某个参数(可能是服务器数据或其他数据),这将非常方便。
如前所述,useGlobalState
接受第二个可选参数,它是一个配置对象,default
是可用配置之一。
default
配置用于指定默认值,以便useGlobalState
在无法找到第一个参数中指定的键时创建全局状态。例如
const [user, setUser, updateUser] = useGlobalState("user", {default: null});
这段代码的意思是,如果商店中没有键“用户”,则获取该键的全局状态,创建一个并为其分配值null
。
即使您没有创建名为“用户”的全局状态,这段代码也会起作用,如果找不到它,它只会创建一个,并null
按照您指定的默认值为其分配。
useGlobalStateReducer
useGlobalStateReducer
工作原理与 hook 类似useReducer
,但它接受一个 reducer 和一个全局状态,或者作为全局状态的 key(name)。除了上述两个参数外,它还接受另一个可选参数,即配置对象,就像在、和useGlobalState
中一样(稍后将讨论)。例如,如果您有一个类似这样的 store 设置selector
patcher
default
persist
const user = {
name: "Yezy",
age: 25,
email: "yezy@me.com"
}
store.setState("user": user);
您可以使用useGlobalStateReducer
钩子来获取功能组件中的全局状态,例如
function myReducer(state, action){
// This could be any reducer
// Do whatever you want to do here
return newState;
}
const [name, dispatch] = useGlobalStateReducer(myReducer, "user");
正如您所见,这里的一切都像在useReducer
钩子中一样工作,所以如果您知道useReducer
这一点,应该很熟悉。
以下是useGlobalStateReducer
useGlobalStateReducer(reducer: Function, globalState|key: GlobalState|String, {default: Any, persist: Boolean, selector: Function, patcher: Function})
状态持久性
有时您可能希望将全局状态保存在本地存储中,可能是因为您可能不想在应用程序关闭时丢失它们(即,您想在应用程序启动时保留它们)。
状态池可以很容易地将您的全局状态保存在本地存储中,您需要做的就是在创建全局状态时使用persist
配置来告诉状态池将您的全局状态保存在本地存储中。
无需担心更新或加载您的全局状态,state-pool已经为您处理了这些,以便您可以专注于使用您的状态。
store.setState
接受第三个可选参数,即配置对象,persist
该配置用于告诉状态池是否将您的状态保存在本地存储中。即
store.setState(key: String, initialState: Any, {persist: Boolean})
由于state-pool允许您动态创建全局状态,因此它还允许您根据需要将这些新创建的状态保存在本地存储中,这就是为什么和都useGlobalState
接受useGlobalStateReducer
持久配置,就像在store.setState
它用于告诉state-pool是否将新创建的状态保存在本地存储中。即
useGlobalState(key: String, {defaultValue: Any, persist: Boolean})
useGlobalStateReducer(reducer: Function, key: String, {defaultValue: Any, persist: Boolean})
persist
默认情况下,所有情况下的值为false
(这意味着它不会将全局状态保存到本地存储),因此如果要激活它,请将其设置为。state -pooltrue
的优点在于,您可以自由选择在本地存储中保存哪些内容,不保存哪些内容,因此您无需将整个存储都保存在本地存储中。
将状态存储到本地存储时,localStorage.setItem
不应调用过于频繁,因为它会触发昂贵的JSON.stringify
操作来序列化全局状态以将其保存到本地存储。
了解状态池附带的store.LOCAL_STORAGE_UPDATE_DEBOUNCE_TIME
变量,该变量用于设置全局状态更改时将状态更新到本地存储的去抖时间。默认值为 1000 毫秒,相当于 1 秒。如果您不想使用默认值,可以自行设置。
非基于键的全局状态
状态池不会强迫您使用基于密钥的全局状态,如果您不想使用store
来保留全局状态,那么选择权在您手中
以下是如何使用非基于键的全局状态的示例
// Example 1.
import React from 'react';
import {createGlobalState, useGlobalState} from 'state-pool';
let count = createGlobalState(0);
function ClicksCounter(props){
const [count, setCount, updateCount] = useGlobalState(count);
let incrementCount = (e) => {
setCount(count+1)
}
return (
<div>
Count: {count}
<br/>
<button onClick={incrementCount}>Click</button>
</div>
);
}
ReactDOM.render(ClicksCounter, document.querySelector("#root"));
// Example 2
const initialGlobalState = {
name: "Yezy",
age: 25,
email: "yezy@me.com"
}
let user = createGlobalState(initialGlobalState);
function UserName(props){
const selector = (user) => user.name; // Subscribe to user.name only
const patcher = (user, name) => {user.name = name}; // Update user.name
const [name, setName, updateName] = useGlobalState(user, {selector: selector, patcher: patcher});
let handleNameChange = (e) => {
setName(e.target.value);
// updateName(name => e.target.value); You can do this if you like to use `updatName`
}
return (
<div>
Name: {name}
<br/>
<input type="text" value={name} onChange={handleNameChange}/>
</div>
);
}
结论
感谢您提出这一点,我想听听您的意见,您对这种方法有何看法?
如果您喜欢该库,请在https://github.com/yezyilomo/state-pool上给它一个星星。
文章来源:https://dev.to/yezyilomo/you-can-definitely-use-global-variables-to-manage-global-state-in-react-17l3