发布于 2026-01-06 1 阅读
0

使用全局变量和 Hooks 在 React 中进行全局状态管理。状态管理不必那么复杂。简介 那么,有哪些新内容? 展示代码 如果组件需要共享状态该怎么办? 由 Mux 呈现的 DEV 全球项目展示挑战赛:展示你的项目!

使用全局变量和 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"));


Enter fullscreen mode Exit fullscreen mode

正如你可能已经猜到的,这个例子可以渲染,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"));


Enter fullscreen mode Exit fullscreen mode

这样就行了,每次点击都会重新渲染。

我知道,我知道这不是在 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"));


Enter fullscreen mode Exit fullscreen mode

这里有两个组件Counter1A 和 B Counter2,它们都使用了counter全局状态。但是,当你点击 A 上的按钮时,它只会更新A 上Counter1的值。B 上的值将保持为 0。现在,当你点击 B 上的按钮时,它会更新,但会从 0 跳到 A 的最后一个值加 1。如果你返回到 A ,它也会发生同样的情况,从上次结束的位置跳到最后一个值加 1。countCounter1counter2Counter2Counter1Counter1Counter2

嗯……这很奇怪,可能是什么原因造成的呢?

原因在于,当你点击按钮时,Counter1它会递增值count,但它只会重新渲染Counter1,因为Counter1Counter2没有共享重新渲染的方法,每个都有自己的incrementCount方法,当点击其中的按钮时,该方法会运行。

现在,当你点击它时,它会运行Counter2一个程序,该程序会获取一个已经递增的incrementCount值,并将其再次递增,然后重新渲染,这就是为什么计数的值会跳到上次的值加一的原因。如果你返回,同样的事情也会发生。countCounter1Counter1Counter1

所以问题在于,当一个组件更新全局状态时,其他共享该全局状态的组件并不知道,只有更新全局状态的组件才知道。因此,当全局状态更新时,其他共享该全局状态的组件不会重新渲染。

那么我们该如何解决这个问题呢?

乍一看似乎不可能,但如果你仔细观察,就会发现一个非常简单的解决方法。

由于全局状态是共享的,因此解决此问题的办法是让全局状态通知所有(共享它的)组件它已更新,以便它们都需要重新渲染。

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

为了简化流程,具体步骤如下

  1. 创建全局状态(严格来说,它是一个全局变量)

  2. 将一个或多个组件订阅到已创建的全局状态(这样全局状态就可以跟踪所有订阅它的组件)。

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

  4. 当全局状态收到更新请求时,它会执行更新并通知所有订阅该状态的组件进行更新(重新渲染)。

以下是架构图,以便更清楚地说明。
架构图

你可能已经熟悉这种设计模式了,它非常流行,叫做观察者设计模式

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

我们先来实现全局状态。



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


Enter fullscreen mode Exit fullscreen mode

根据上述实现,今后创建全局状态的方式如下所示。



const count = new GlobalState(0);
// Where 0 is the initial value


Enter fullscreen mode Exit fullscreen mode

至此,全局状态的实现就完成了,总结一下我们所做的工作:GlobalState

  1. 我们创建了一种通过订阅和取消订阅全局状态的subscribe机制unsubscribe

  2. setValue我们创建了一种机制,当全局状态更新时,通过方法通知订阅者。

  3. getValue我们创建了一种通过方法获取全局状态值的机制。

现在我们需要实现一种机制,允许我们的组件订阅、取消订阅并从中获取当前值GlobalState

如前所述,我们希望我们的 API 像 Hooks API 一样简单易用、直观明了。因此,我们将创建一个useState类似 Hook 的全局状态管理 Hook。

我们将称之为useGlobalState……

它的用法将如下:



const [state, setState] = useGlobalState(globalState);


Enter fullscreen mode Exit fullscreen mode

现在我们来写吧……



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


Enter fullscreen mode Exit fullscreen mode

这就是我们的钩子函数正常工作所需的全部内容。钩子函数最重要的部分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"));


Enter fullscreen mode Exit fullscreen mode

你会发现这个例子运行得非常完美。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