React - 关于 state 的保留以及重置

1,197 阅读4分钟

前言

React 使用树状结构来对你的代码进行管理,将 JSX 生成 UI 树,并且根据 UI 树去更新浏览器的 DOM 元素。

Pasted image 20230823173425.png

根据组件在 UI 树中的位置,React 可以跟踪哪些 state 属于哪个组件。你可以控制在重新渲染过程中何时对 state 进行保留和重置。

或许说到这里,你会觉得,在你为一个组件添加 state 的时候,他就是属于这个组件的,但是事实真是如此吗,并不是,实际上 satae 是被保存在 React 内部,根据当前这个组件在 UI 树的位置,React 会将 它 与 对应的 state 关联起来。

你会说这和存在于组件内部有什么区别,请往下看。

不同组件的state

import { useState } from 'react';

export default function App() {
  const [showB, setShowB] = useState(true);
  return (
    <div>
      <Counter />
      {showB && <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        渲染第二个计数器
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        加一
      </button>
    </div>
  );
}

很明显这会产生两个不同的盒子,并且他们有着属于自己的状态,互不干扰。

Pasted image 20230825104746.png

在你关闭掉第二个盒子的渲染并且重新渲染的时候,state 会被重置,这也没有问题

动画1.gif

相同位置的相同组件的 state

接下去的例子就能够体现出来,state 保存在组件内部以及保存在 React 内部,会造成什么样的不同

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />        
        渲染有阴影的组件
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        加一
      </button>
    </div>
  );
}

动画3.gif

你可以会以为在勾选复选框的时候 Counter 组件的state会被重置,但是很明显,虽然在代码中

{isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}

我们写了两个子组件,但是对于react来说,他们两个是完全相同的,保留了state的状态

后面我又写了一个简单的vue项目验证了一下,这个问题在vue中也是一样的,这可能就是在编译器编译完我们写的代码以后,丢给渲染器的前后组件时完全一样的(渲染器不去关心你是哪个子组件,他只关心拿到的组件结构跟之前是否有出入,是否需要重新渲染)。

298382ab3f5d8aaa1da1e83213bfd49.png

cd7122fa1f6fad0a5be2596c5929a57.png

动画2.gif

后面我在子组件中定义了 mounted 钩子也证实了这个事,子组件并没有被销毁重建,因为这个钩子就只有在最开始的时候执行了一次!

f89d47d6f11cc217ed8d2985db20d08.png

  1. 想要避免这种情况,我们可以用不同的节点包裹上子组件,
{isFancy ? (
		<div>
			<Counter isFancy={true} />
		</div>
	) : (
		<section>
			<Counter isFancy={false} />
		</section>
	)}
  1. 或者给两个子组件设置不同的 key。这也是为什么我们在使用 react 或者 vue 的循环生成的时候,要为每一个子组件设置一个不同的 key。
{isFancy ? (
		<Counter key="1"isFancy={true} />
	) : (
		<Counter key="2"isFancy={false} />
	)}
  1. 在 react 中,还可以在不同的地方渲染这两个组件,但是这在vue中好像是行不通的,因为上面的vue例子中使用的就是这样的写法。
{isFancy && (
	<Counter isFancy={true} />
)} 
{!isFancy && (
	<Counter isFancy={false} />
)}

如何为移除的组件保留状态

在实际的使用的时候,你可能希望两个切换的tab虽然是属于同一个组件,但是他们要有着自己的状态,这时候我们可以不选择移除这个组件,而是选择隐藏它,类似于 vue 中的 v-show 来代替 v-if。

但是要注意的是,这种用法要是过多,可能会导致性能上的问题,所以在实际的使用场景当中,应该要根据自己的需求来选择要如何使用,以及是否保留组件状态。

插曲,为什么不要用index作为列表的key

当你使用index作为列表的key的时候,说明你的列表都是一样的组件,万一你的组件内部存在状态,并且你的这个列表是可以切换顺序的,那么就会导致状态混乱,因为对于react来说,组件都是一样的,index为key代表组件的位置从来没变化过,一旦变化了,那么状态也只会对号入座的赋值。

参考

Preserving and Resetting State