第二章: 使用局部状态与全局状态

React 的组件本质上是一个树结构。在树结构中,在某个子树中生成一个状态是很常见的:你在某个更高层的组件定义了一个状态,然后这个状态在这个组件及其子组件中被消费了。这种实践队维持代码的本地性和可复用性是很有帮助的。

但是,在特定场景中,我们有一个状态需要中两个,甚至更多个距离远的地方被消费,这就是全局状态登场的时候了。不像本地状态,全局状态并不显示的属于一个特定的组件,所以把全局状态存在哪里本身是一个值得考虑的点。

在这一章节,我们会学习本地状态,以及一些提状态提升的技巧。状态提升是把状态放在组件树更高层级的一个技巧。之后,我们会学习全局状态,并思考如何使用他们。

我们会讨论下面几个议题:

  • 理解本地状态
  • 如何高效使用本地状态
  • 使用全局状态

理解何时使用本地状态

在讨论React之前,我们先看看JS的函数是如何工作的。JS函数可能是纯函数,也可能是非纯函数。一个纯函数之依赖输入的参数:同样的输入,永远会返回同样的输出。而依赖了状态的函数,就不再是纯函数了。React 组件也是函数,当然也可以是纯函数。如果我们在React 组件中使用了 状态,那它就不再是纯函数了。如果我们在一个 React 组件中使用状态,那么该组件就会是不纯的。然而,如果这个状态是组件局部的,它就不会影响其他组件,我们把这种特性称为 "封装性(自包含性)"。

在这个部分,我们会学习JS,以及JS函数与函数组件的相似性。之后,我们会讨论一个本地状态是如何在概念上被实现的。

函数与参数

在JS中,一个函数接受一个参数,并返回一个值。下面是一个例子:

js 复制代码
const addOne = (n) => n + 1;

对于一个纯函数来说,相同的输入总会有相同的输出。人们推荐写纯函数,就是因为纯函数的行为更加可预测。

如果一个函数依赖全局变量,就是这样:

js 复制代码
let base = 1;

const addBase = (n) => n + base;

只要 base不变,这个addBase函数就和addOne运行得一样。然而,如果base为2,它们运行地就不一样了。其实这并不是啥坏事,事实上这是一个很强的特性,你可以通过函数外的变量来修改函数内的行为。但其缺点在于,在不了解 addBase 函数依赖于外部变量的情况下,你不能简单地获取该函数并随意在其他地方使用它。如你所见,这是一种权衡。

如果 base 是一个单例(内存中的单个值),那么这并不是一种理想的模式,因为这样代码的可复用性会降低。为了避免使用单例并在一定程度上减轻其缺点,一种更具模块化的方法是创建一个容器对象,如下所示:

js 复制代码
const createContainer = () => {
    let base = 1;
    const addBase = (n) => n + base;
    const changeBase = (b) => { base = b; };
    
    return { addBase, changeBase }
}

const { addBase, changeBase } = createContainer();

如此一来,这段代码就不是单例模式了,你想要多少个container就创造多少个了。与把base作为全局单例相比,container模式更加独立也更可复用。你可以在不影响已经有container的前提下,在其他地方用生成另一个container。

一个小提示:尽管容器中的 addBase 函数从数学意义上来说并非纯函数,但如果 base 的值没有改变,你通过调用 addBase 函数仍能得到相同的结果(这种特性有时被称为幂等性)。

React函数与属性

React在概念上而言,是一个把状态转化为用户界面的函数。当你写React代码时,React组件是一个函数,而这个函数的参数是属性。

一个展示数字的组件是这样的:

js 复制代码
const Component = ({ number }) => {
    return <div>{number}</div>;
}

这个组件接收一个数字作为参数,并返回一个JSX元素。

现在,我们来另一个例子:

js 复制代码
const Component = ({ number }) => {
    return <div>{number} + 1</div>;
}

这个组件接收一个数字number,并展示这个number + 1的结果。它就像addOne一样,是一个纯函数。要说不同的话,就是一个返回的是数字,而另一个是JSX。

理解用useState实现本地状态

如果我们用useState来创建本地状态,会怎样?让我们创建一个基准状态,然后让这个basenumber属性相加:

js 复制代码
const AddBase = ({ number }) => {
    const [base, changeBase] = useState(1);
    return <div>{number + base}</div>;
}

因为这个函数已经依赖了不属于参数里的base,这个函数已经不是纯函数了。

AddBase组件中的useState是用来干嘛的?让我们回一下上一部分的createContainer。正如createContainer返回了basechangeBaseuseState也返回了basechangeBase。虽然我们没有看到basechangeBase的完整实现,但它们在概念上和 createContainer 返回的是一样的。

如果我们假定useState的行为,即它在未被更改时返回初始值(base),那么AddBase函数是幂等的,就如同我们在createContainer函数中所看到的情况一样。

这个结合了useStateAddBase函数是封闭的,因为changeBase仅在函数声明的作用域内可用。在该函数外部是无法更改base的。这里对useState的使用属于局部状态,并且由于它是封闭的,不会对组件外部的任何事物产生影响,所以保证了局部性;只要合适,这种使用方式是值得推荐的。

局部状态的局限

什么时候局部状态不合适呢?当我们想要打破局部性时,局部状态就不合适了。在AddBase组件的例子中,当我们想从代码中完全不同的部分去更改base时,局部状态就不合适。如果你需要从函数组件外部更改状态,这时就需要用到全局状态了。

状态变量在概念上是一个全局变量。全局变量对于从函数外部控制 JavaScript 函数的行为很有用。同样地,全局状态对于从组件外部控制 React 组件的行为也很有用。然而,使用全局状态会让组件的行为更难以预测。这是一种权衡。我们不应过度使用全局状态。应将使用局部状态作为主要方式,仅在次要情况下使用全局状态。从这个意义上说,了解局部状态能涵盖多少种用例是很重要的。

在这一部分,我们学习了 React 中的局部状态以及 JavaScript 函数。接下来,我们将学习一些使用局部状态的模式。

有效地使用本地状态

为了更有效的使用本地状态,你需要知道一些模式。在这个部分,我们需要学习如何提升状态:这意味着把状态定义在组件树中更高级的位置;还要学习如何提升内容,这意味着把内容定义在组件树中更高级的位置。

提升状态

假设我们有两个计数器组件:

js 复制代码
const Component1 = () => {
    const [count, setCount] = useState(0);

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>

        </div>
     );
};

const Component2 = () => {
    const [count, setCount] = useState(0);

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>

        </div>
     );
};

因为这两个组件有着各自独立的状态,所以这两个count是独自运作的。如果我们想让这两个组件共享一套状态,我们可以创造一个父组件,并把状态放在这里。

下面是例子,父组件会把count,setCount作为属性传给Component1Component2:

js 复制代码
const Component1 = ({ count, setCount }) => {

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>

        </div>
     );
};

const Component2 = ({ count, setCount }) => {

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>

        </div>
     );
};

const Parent = () => {
    const [count, setCount] = useState(0);
    
    return (
        <>
            <Component1 count={count} setCount={setCount} />
            <Component2 count={count} setCount={setCount} />
        </>
    )
}

因为count定义在Parent组件,所以这个count状态是被Component1Component2所共享的。它是组件的一个局部状态;这个组件的子组件可以通过属性的方式消费这个状态。

这个模式适用于大多数本地状态;但是,这个模式也会带来一些性能问题。如果把状态提升了,父组件的重新渲染,会导致子组件一同重新渲染。在某些场景下,这会带来性能问题。

提升内容

在处理复杂的组件树时,我们也许有一个组件并不依赖被提升的状态。

在下面这个例子中,我们在原来的Component1中添加一个AdditionalInfo组件:

js 复制代码
const AdditionalInfo = () => {
    return <p>Some information</p>
};

const Component1 = ({ count, setCount }) => {

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>
            <AdditionalInfo />
        </div>
     );
};

const Component2 = ({ count, setCount }) => {

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>

        </div>
     );
};

const Parent = () => {
    const [count, setCount] = useState(0);
    
    return (
        <>
            <Component1 count={count} setCount={setCount} />
            <Component2 count={count} setCount={setCount} />
        </>
    )
}

如果count变化了,Parent重新渲染了,之后Component1Component2,和 AdditionalInfo也重新渲染。然而,AdditionalInfo组件并不需要重新渲染,因为它并不依赖count状态。如果AdditionalInfo组件会带来严重的性能问题,那么其重新渲染应该被尽量减少。

为了避免重新渲染,我们可以提升这个内容:

js 复制代码
const AdditionalInfo = () => {
    return <p>Some information</p>
};

const Component1 = ({ count, setCount, additionalInfo }) => {

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>
            {additionalInfo}
        </div>
     );
};

const Component2 = ({ count, setCount }) => {

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>

        </div>
     );
};

const Parent = ({ additionalInfo }) => {
    const [count, setCount] = useState(0);
    
    return (
        <>
            <Component1
                count={count}
                setCount={setCount}
                additionalInfo={additionalInfo}
            />
            <Component2 count={count} setCount={setCount} />
        </>
    )
}

const GrandParent = () => {
    return <Parent additionalInfo={<AdditionalInfo />} />
}

GrandParent组件有additionalInfo属性,additionalInfo属性会传递给子组件。如此一来,AdditionalInfo组件不会因为count的变化而重新渲染了。

这种情况的一种变体是使用children属性。下面这个使用children属性的示例与前面的示例等效,不过编码风格有所不同:

js 复制代码
const AdditionalInfo = () => {
    return <p>Some information</p>
};

const Component1 = ({ count, setCount, children }) => {

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>
            {children}
        </div>
     );
};

const Component2 = ({ count, setCount }) => {

    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>

        </div>
     );
};

const Parent = ({ children }) => {
    const [count, setCount] = useState(0);
    
    return (
        <>
            <Component1 count={count} setCount={setCount}>
                {children}
            </Component1>
            <Component2 count={count} setCount={setCount} />
        </>
    )
}

const GrandParent = () => {
    return (
        <Parent>
            <AdditionalInfo />
        </Parent>
    );
}

children 是一个特殊的属性名称,在 JSX 格式中它表现为嵌套的子元素。如果你有多个元素要传递,给属性命名会更合适。这在很大程度上是一种风格上的选择,开发人员可以采用他们喜欢的任何一种方式。

在这个部分,我们已经讨论了如何有效地使用本地状态。如果我们能够合适地提升状态和内容,我们可以用本地状态处理很多问题。接下来,我们要学习如何使用全局状态。

使用全局状态

在这个部分,我们会学习什么是全局状态,以及应该何时使用全局状态。

什么是全局状态

在本书中,一个全局状态意味着,它不是本地状态。如果一个状态属于一个组件,被封装在一个组件内,那它是局部状态。因此,如果一个状态并不属于一个组件,而且它可以被多个组件使用,它就是全局状态。

可能存在一种所有组件都依赖的应用程序级局部状态。在这种情况下,应用程序级的局部状态可以被视为全局状态。从这个意义上讲,我们无法清晰地区分局部状态和全局状态。在大多数情况下,如果你考虑某个状态在概念上属于哪里,你就能判断出它是局部状态还是全局状态。

在讨论全局状态时,我们要考虑这些方面

  • 第一个是单例模式,这意味在一些上下文,状态只有一个值
  • 第二个方面是共享状态,这位置这个状态被多个组件所共享,但是它需要在JS内存中只有一个值。一个全局状态并不是单例,可以有多个值。

为了说明非单例全局状态是如何工作的,这里有一个示例来展示 JavaScript 中的一个非单例变量:

js 复制代码
const createContainer = () => {
    let base = 1;
    const addBase = (n) => n + base;
    const changeBase = (b) => { base = b; };
    return { addBase, changeBase };
};
const container1 = createContainer();
const container2 = createContainer();

container1.changeBase(10);

console.log(container1.addBase(2)); // shows "3"
console.log(container2.addBase(2)); // shows "12"

在这个例子中,basecontainer内的局部变量。因为base在每一个container内都是孤立的,所以在container1内的base的变化,并不会影响到container2内的base

在React,概念是类似的。如果一个全局状态是单例,我们在记忆中只有一个值。如果全局状态不是单例模式,那么组件树上不同的组件就有不同的值。

何时使用全局状态

在 React 中,当出现以下两种情况时,我们就需要用到全局状态,具体准则如下:

・当不适合传递属性(props)时

・当我们在 React 之外已经有了一个状态时

当不适合传递属性(props)时

如果一个状态在组件树两个的地方被消费,而且这两个地方距离太远了,那么再使用属性传递就不合适了。

下面是一个例子,我们有一个三层深的树,我们需要自顶向下传递状态,会是这样:

js 复制代码
const Component1 = ({ count, setCount }) => {
    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>
        </div>
    );
};
    
    const Parent = ({ count, setCount }) => {
    return (
        <>
            <Component1 count={count} setCount={setCount} />
        </>
    );
};

const GrandParent = ({ count, setCount }) => {
    return (
        <>
            <Parent count={count} setCount={setCount} />
        </>
    );
};

const Root = () => {
 const [count, setCount] = useState(0);
 return (
     <>
         <GrandParent count={count} setCount={setCount} />
     </>
 );
};

从局部性的角度来看,这完全没问题,甚至是值得推荐的做法;然而,让中间组件专门用于传递属性可能会过于繁琐。通过多级中间组件传递属性,可能不会带来良好的开发者体验,因为这看起来像是不必要的额外工作。此外,当状态更新时,中间组件会重新渲染,这可能会影响性能。

在这个例子中,使用全局状态是更加合适的,这样就不需要跨越组件层级传递属性了.

下面是一个用伪代码实现全局全局状态的例子:

js 复制代码
const Component1 = () => {
    // useGlobalCountState is a pseudo hook 
    const [count, setCount] = useGlobalCountState();
    return (
        <div>
            {count}
            <button onClick={() => setCount((c) => c + 1)}>
                Increment Count
            </button>
        </div>
    );
};

const Parent = () => {
    return (
        <>
            <Component1 />
        </>
    );
};

const GrandParent = () => {
    return (
        <>
            <Parent />
        </>
    );
};

const Root = () => {
    return (
        <>
            <GrandParent />
        </>
    );
};

在这个例子中,只有Component1使用了全局状态。这样就不用烦琐的进行跨组件传值了。

当我们在 React 之外已经有了一个状态时

这一些情况下,你有些位于React外的全局状态。比如说,你的应用有在React外获得的拥护下信息。在这个情况下,全局状态应该在React外,而这些用于信息应当存在全局状态中。

下面是一个由伪代码实现的例子:

js 复制代码
const globalState = {
    authInfo: { name: 'React' },
};
const Component1 = () => {
    // useGlobalState is a pseudo hook
    const { authInfo } = useGlobalState();
    
    return (
        <div>
            {authInfo.name}
        </div>
    );
};

在这个例子中,globalState 存在且是在 React 外部定义的。useGlobalState 是一个钩子函数,它会连接到 globalState,并且能够在 Component1 中提供 authInfo

在这一部分,我们了解到全局状态是一种不能作为局部状态的状态。全局状态主要是作为局部状态的补充来使用的,并且有两种情况下使用全局状态效果很好:一种是在传递属性没有意义的情况下,另一种是应用程序中已经存在一个全局状态的情况。

概要

在本章中,我们讨论了局部状态和全局状态。只要有可能,局部状态是更可取的选择,并且我们学习了一些有效使用局部状态的技巧。然而,全局状态在局部状态无法发挥作用的地方能派上用场,这就是为什么我们要研究何时应该使用全局状态。

在接下来的三章中,我们将学习在 React 中实现全局状态的三种模式;具体来说,在下一章中,我们将从利用 React 的上下文(context)开始学习。

相关推荐
天若有情6739 分钟前
《可爱风格 2048 游戏项目:HTML 实现全解析》
前端·游戏·html
tryCbest9 分钟前
前端知识-CSS(二)
前端·css
好_快16 分钟前
Lodash源码阅读-setToArray
前端·javascript·源码阅读
好_快21 分钟前
Lodash源码阅读-mapToArray
前端·javascript·源码阅读
子洋25 分钟前
Agent TARS 评测:开源版 Manus?实际体验告诉你答案!
前端·人工智能·后端
前端切图仔0011 小时前
Vue 3 项目实现国际化指南 i18n
前端·javascript·vue.js
Yvette-W4 小时前
【React】List使用QueueAnim动画效果不生效——QueueAnim与函数组件兼容性问题
前端·javascript·react.js·前端框架·list
zy0101015 小时前
JSX入门
前端·css·react.js·html·jsx
Blue.ztl5 小时前
菜鸟之路Day25一一前端工程化(二)
开发语言·前端·javascript
阿珊和她的猫6 小时前
Vue.js的ref:轻松获取DOM元素的魔法
前端·javascript·vue.js