React的State

25 阅读2分钟

State

跟Vue的data相似, 但又有不同.State是一个快照, 对应于组件某一时刻的状态, 修改State的变量不会更改已经存在的state变量, 但是会出发重新渲染.

为何要使用State

  1. state能保存渲染间的数据
  2. state的修改能触发更新

以下示例1中, 点击Next按钮预期是可以累加, 但是却不能所愿.原因是点击Next按钮并没有触发更新, index变量的累加值没有渲染到页面上.

function Gallery() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }

  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      {index}
    </>
  );
}

以下示例2就可以实现以上例子的功能

import { useState } from 'react';

export default function Gallery() {
  const [index, setIndex] = useState(0);

  function handleClick() {
    setIndex(index + 1);
  }

  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      {index}
    </>
  );
}

State是一个快照

设置它不会更改你已有的 state 变量,但会触发重新渲染。

下面的示例3不能完成预期的功能, 预期点击+3按钮, 会累加3, 但是实际情况是累加1

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

原因如下:

  1. State是一个快照
  2. React会对State的更新做批处理,当批处理完成后UI才会更新
  3. setNumber(number + 1);连续运行3次, 又因为在此次渲染中, number的快照值是0, 所以批处理实际执行的代码为如下代码, 最终number被更新为了1
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

如何在下次渲染前多次更新同一个State呢

答案是设置器中不再传入特定的值, 而是传入一个函数, 函数接收React传入的当前快照的State更新队列的上一个state变量值

下面示例4可以完成示例3的功能

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

下面是示例5可以进一步验证, 最后运行结果为42

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

State中更新对象和数组

更新不建议直接修改源对象和数组, 而是应该更新源对象和数组的副本, 然后将副本赋值给源对象和数组.

可以使用Immer库完成快速的更新

示例6, 使用了对象的展开符号创建对象副本, 最终副本赋值给源persion

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

示例7, 使用Immer, 更加简洁了

import { useImmer } from 'use-immer';

export default function Form() {
  const [person, updatePerson] = useImmer({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    updatePerson(draft => {
      draft.name = e.target.value;
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}