useState

使用 state 需要注意以下几点:

  • 当一个组件需要在多次渲染记住某些信息时,使用 state 变量。

  • 调用 Hook 时,包括 useState,仅在组件或者另一个 Hook 的顶层作用域调用。

  • state 是隔离且私有的。也就是说,将一个组件调用两次,他们内部的 state 不会互相影响。

1. state 如同一张快照

当 React 重新渲染一个组件时:

  1. React 会再次调用你的函数
  2. 你的函数会返回新的 JSX
  3. React 会更新界面来匹配你返回的 JSX

作为一个组件的记忆,state 不同于在你的函数返回之后就会消失的普通变量。state 实际上“活”在 React 本身中——就像被摆在一个架子上!——位于你的函数之外。当 React 调用你的组件时,它会为特定的那一次渲染提供一张 state 快照。你的组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值 被计算出来的!

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 1);
          setNumber(number + 1);
          setNumber(number + 1);
        }}>
        +3
      </button>
    </>
  );
}

以下是这个按钮的点击事件处理函数通知 React 要做的事情:

  1. setNumber(number + 1)number 是 0 所以 setNumber(0 + 1)。 React 准备在下一次渲染时将 number 更改为 1。
  2. setNumber(number + 1)number 是 0 所以 setNumber(0 + 1)。 React 准备在下一次渲染时将 number 更改为 1。
  3. setNumber(number + 1)number 是 0 所以 setNumber(0 + 1)。 React 准备在下一次渲染时将 number 更改为 1。

尽管你调用了三次 setNumber(number + 1),但在这次渲染的 事件处理函数中 number 会一直是 0,所以你会三次将 state 设置成 1。这就是为什么在你的事件处理函数执行完以后,React 重新渲染的组件中的 number 等于 1 而不是 3。

为了更好理解,我们看下面这个例子:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 5);
          setTimeout(() => {
            alert(number);
          }, 3000);
        }}>
        +5
      </button>
    </>
  );
}

点击+5 后,弹出的数字是 0,而不是 5. 点击按钮后的操作:

  1. setNumber(0+5)
  2. js setTimeout(() => { alert(0) })

2. 将 state 加入队列

React 会对 state 更新进行批处理。在上面的示例中,连续调用了三次setNumber(number + 1)并不能得到我们想要的结果。

React 会等到事件处理函数中的所有代码都运行完毕再处理你的 state 更新。

我们可以通过更新函数来在下次渲染之前多次更新同一个 state。比如:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber((n) => n + 1);
          setNumber((n) => n + 1);
          setNumber((n) => n + 1);
        }}>
        +3
      </button>
    </>
  );
}

下面是 React 在执行事件处理函数时处理这几行代码的过程:

  1. setNumber(n => n + 1)n => n + 1 是一个函数。React 将它加入队列。
  2. setNumber(n => n + 1)n => n + 1 是一个函数。React 将它加入队列。
  3. setNumber(n => n + 1)n => n + 1 是一个函数。React 将它加入队列。

当你在下次渲染期间调用 useState 时,React 会遍历队列。之前的 number state 的值是 0,所以这就是 React 作为参数 n 传递给第一个更新函数的值。然后 React 会获取你上一个更新函数的返回值,并将其作为 n 传递给下一个更新函数,以此类推:

更新队列n返回值
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

看看下面这个例子:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 5);
          setNumber((n) => n + 1);
          setNumber(42);
        }}>
        增加数字
      </button>
    </>
  );
}

以下是 React 在执行事件处理函数时处理这几行代码的过程:

  1. setNumber(number + 5)number 为 0,所以 setNumber(0 + 5)。React 将 “替换为 5” 添加到其队列中。
  2. setNumber(n => n + 1)n => n + 1 是一个更新函数。React 将该函数添加到其队列中。
  3. setNumber(42):React 将 “替换为 42” 添加到其队列中。

在下一次渲染期间,React 会遍历 state 队列:

更新队列n返回值
替换为 505
n => n + 155 + 1 = 6
替换为 42642

可以这样来理解状态队列的更新:

function getFinalState(baseState, queue) {
  let finalState = baseState;
  queue.forEach((update) => {
    finalState = typeof update === 'function' ? update(finalState) : update;
  });
  return finalState;
}

其中 baseState 是初始状态,queue 是状态更新队列,包括数据和更新函数。

3. set 函数一定会触发更新吗?

看下面这个例子:

export default function Counter() {
  const [number, setNumber] = useState(0);
  const [person, setPerson] = useState({ name: 'jack' });
  console.log('渲染');

  return (
    <>
      <button
        onClick={() => {
          setNumber(number);
        }}>
        增加数字
      </button>
      <h1>{number}</h1>
      <button
        onClick={() => {
          person.age = 18;
          setPerson(person);
        }}>
        修改对象
      </button>
      <h1>{JSON.stringify(person)}</h1>
    </>
  );
}

组件的更新意味着组件函数的重新执行。对于上面这个例子,无论是点击 增加数字 还是 改变对象 都没有打印 渲染

set 函数触发更新的条件:

  • 值类型,state 的值改变
  • 引用类型,state 的引用改变

对于上面的例子:

  • number 是值类型。点击增加数字,值没有改变,不会触发更新。
  • person 是引用类型。点击修改对象,虽然 person 对象的值虽然变化了,但是引用地址没有变化,因此也不会触发更新。

4. 构建 state 的原则

  1. 合并关联的 state

有时候我们可能会不确定使用单个 state 还是多个 state 变量。

const [x, setX] = useState(0);
const [y, setY] = useState(0);

const [position, setPosition] = useState({ x: 0, y: 0 });

从技术上讲,我们可以使用其中任何一种方法。但是,如果某两个 state 变量总是一起变化,则将它们统一成一个 state 变量可能更好。这样你就不会忘记让它们始终保持同步。

  1. 避免矛盾的 state
import { useState } from 'react';

export default function Send() {
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          setIsSent(false);
          setIsSending(true);
          setTimeout(() => {
            setIsSending(false);
            setIsSent(true);
          }, 2000);
        }}>
        发送
      </button>
      {isSending && <h1>正在发送...</h1>}
      {isSent && <h1>发送完成</h1>}
    </>
  );
}

尽管这段代码是有效的,但也会让一些 state “极难处理”。例如,如果你忘记同时调用 setIsSentsetIsSending,则可能会出现 二者 同时为 true 的情况。

可以用一个 status 变量来代替它们。代码如下:

import { useState } from 'react';

export default function Send() {
  const [status, setStatus] = useState('init');
  return (
    <>
      <button
        onClick={() => {
          setStatus('sending');
          setTimeout(() => {
            setStatus('sent');
          }, 2000);
        }}>
        发送
      </button>
      {status === 'sending' && <h1>正在发送...</h1>}
      {status === 'sent' && <h1>发送完成</h1>}
    </>
  );
}
  1. 避免冗余的 state
import { useState } from 'react';

export default function Name() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  return (
    <>
      <span>First Name</span>
      <input
        value={firstName}
        onChange={(e) => {
          setFirstName(e.target.value);
          setFullName(e.target.value + ' ' + lastName);
        }}
      />
      <span>Last Name</span>
      <input
        value={lastName}
        onChange={(e) => {
          setLastName(e.target.value);
          setFullName(firstName + ' ' + e.target.value);
        }}
      />
      <h1>{fullName}</h1>
    </>
  );
}

能够看出,fullName 是冗余的 state。我们可以直接:

const fullName = firstName + ' ' + lastName;

无需再把 fullName 存放到 state 中。

  1. 避免重复的 state

有时候,在我们存储的 state 中,可能有两个 state 有重合的部分。这时候我们就要考虑是不是有重复的问题了。

具体例子见这里open in new window

5. 保存和重置 state

前面我们说过,组件内部的 state 是互相隔离的。一个组件 state 的改变不会影响另外一个。然而,我们看下面这个例子open in new window

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






 

































当我们修改了 Counter组件 的 state 后,点击 checkbox 切换到另一个 Counter,旧 Counter 的 state 并没有变为 0,而是保留了下来。如下图:

在 React 中,相同位置的相同组件会使得 state 保留下来。那么怎么才能让上述例子的 state 重置呢?

有两种方法:

1. 将组件渲染在不同的位置

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

2. 使用 key 来标识组件

{
  isFancy ? <Counter isFancy={true} key={fancyTrue} /> : <Counter isFancy={false} key={fancyFalse} />;
}

效果如下: