你绝对可以使用全局变量来管理 React 中的全局状态

2025-05-28

你绝对可以使用全局变量来管理 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"));


Enter fullscreen mode Exit fullscreen mode

您可能已经猜到了,这个示例count: 0最初会进行渲染,但是如果您单击以增加,则渲染的值count不会改变,但控制台上打印的值会改变。

那么为什么尽管我们只有一个计数变量,却仍会发生这种情况呢?

嗯,发生这种情况是因为当单击按钮时,的值count会增加(这就是它在控制台上打印增加的值的原因)但组件Counter不会重新渲染以获取最新的值count

所以这是阻碍我们在 React 中使用全局变量来管理全局状态的唯一问题。


解决方案

由于全局状态在组件之间共享,我们问题的解决方案是让全局状态通知所有依赖它的组件它已被更新,以便所有组件重新渲染以获取新值。

但是为了让全局状态通知所有使用它的组件(订阅它),它必须首先跟踪这些组件。

为了简化,流程如下

  1. 创建一个全局状态(从技术上讲是一个全局变量)

  2. 将组件订阅到已创建的全局状态(这可以让全局状态跟踪所有订阅它的组件)

  3. 如果组件想要更新全局状态,它会发送更新请求

  4. 当全局状态收到更新请求时,它会执行更新并通知所有订阅它的组件,以便它们自行更新(重新渲染)以获取新值

这是用于视觉澄清的架构图
架构图

有了这个以及钩子的一点帮助,我们将能够使用全局变量完全管理全局状态。

幸运的是,我们不需要自己实施这一措施,因为有State Pool为我们提供支持。


介绍State Pool ✨🎉

State Pool是一个基于全局变量和 React Hooks 的 React 状态管理库。它的 API 与 React Hooks 一样简洁直观,因此,如果您曾经使用过 React Hooks(useStateuseReducer),那么您会对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"));


Enter fullscreen mode Exit fullscreen mode

如果你曾经使用过useStateReact 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"));


Enter fullscreen mode Exit fullscreen mode

在这个例子中,一切都与上一个示例相同

在第三行,我们创建一个名为“用户”的全局状态并将{name: "Yezy", age: 25}其设置为其初始值。

在第五行,我们使用useGlobalState名为“用户”的全局状态(我们在第 3 行创建的)挂接到UserInfo组件中。

然而这里我们除了返回一个函数之外还返回一个函数setUserupdateUser该函数用于更新用户对象而不是设置它,尽管您也可以使用它来设置用户对象。

因此这里updateUser用于更新用户对象,它是一个高阶函数,它接受另一个用于更新用户的函数作为参数(另一个函数以用户(旧状态)作为参数)。

因此,要更新用户的任何嵌套值,您只需执行以下操作



updateUser(function(user){
    user.name = "Yezy Ilomo";
    user.age = 26;
})


Enter fullscreen mode Exit fullscreen mode

您还可以返回新状态而不是更改它,即



updateUser(function(user){
    return {
        name: "Yezy Ilomo",
        age: 26
    }
})


Enter fullscreen mode Exit fullscreen mode

因此返回的数组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"));


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

这段代码的意思是,如果商店中没有键“用户”,则获取该键的全局状态,创建一个并为其分配值null

即使您没有创建名为“用户”的全局状态,这段代码也会起作用,如果找不到它,它只会创建一个,并null按照您指定的默认值为其分配。


useGlobalStateReducer

useGlobalStateReducer工作原理与 hook 类似useReducer,但它接受一个 reducer 和一个全局状态,或者作为全局状态的 key(name)。除了上述两个参数外,它还接受另一个可选参数,即配置对象,就像在useGlobalState一样(稍后将讨论)。例如,如果您有一个类似这样的 store 设置selectorpatcherdefaultpersist



const user = {
    name: "Yezy",
    age: 25,
    email: "yezy@me.com"
}

store.setState("user": user);


Enter fullscreen mode Exit fullscreen mode

您可以使用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");


Enter fullscreen mode Exit fullscreen mode

正如您所见,这里的一切都像在useReducer钩子中一样工作,所以如果您知道useReducer这一点,应该很熟悉。

以下是useGlobalStateReducer



useGlobalStateReducer(reducer: Function, globalState|key: GlobalState|String, {default: Any, persist: Boolean, selector: Function, patcher: Function})


Enter fullscreen mode Exit fullscreen mode


状态持久性

有时您可能希望将全局状态保存在本地存储中,可能是因为您可能不想在应用程序关闭时丢失它们(即,您想在应用程序启动时保留它们)。

状态池可以很容易地将您的全局状态保存在本地存储中,您需要做的就是在创建全局状态时使用persist配置来告诉状态池将您的全局状态保存在本地存储中。

无需担心更新或加载您的全局状态,state-pool已经为您处理了这些,以便您可以专注于使用您的状态。

store.setState接受第三个可选参数,即配置对象,persist该配置用于告诉状态池是否将您的状态保存在本地存储中。即



store.setState(key: String, initialState: Any, {persist: Boolean})


Enter fullscreen mode Exit fullscreen mode

由于state-pool允许您动态创建全局状态,因此它还允许您根据需要将这些新创建的状态保存在本地存储中,这就是为什么和都useGlobalState接受useGlobalStateReducer持久配置,就像在store.setState它用于告诉state-pool是否将新创建的状态保存在本地存储中。即



useGlobalState(key: String, {defaultValue: Any, persist: Boolean})


Enter fullscreen mode Exit fullscreen mode


useGlobalStateReducer(reducer: Function, key: String, {defaultValue: Any, persist: Boolean})


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode




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


Enter fullscreen mode Exit fullscreen mode


结论

感谢您提出这一点,我想听听您的意见,您对这种方法有何看法?

如果您喜欢该库,请在https://github.com/yezyilomo/state-pool上给它一个星星

文章来源:https://dev.to/yezyilomo/you-can-definitely-use-global-variables-to-manage-global-state-in-react-17l3
PREV
面向 Web 开发人员的 10 个资源
NEXT
10 个实用的 Git 技巧,助您改善工作流程