useState 是一个 React Hook,允许你在组件中添加一个状态变量

const [state, setState] = useState(initialState)

参考

useState(initialState)

在组件的顶层调用 useState 来声明一个状态变量

import { useState } from 'react';

function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [todos, setTodos] = useState(() => createTodos());
// ...

约定是用 数组解构 将状态变量命名为 [something, setSomething]

请参见下面的更多示例。

参数

  • initialState:你希望状态最初具有的值。它可以是任何类型的值,但函数有一个特殊的行为。此参数在初始渲染后将被忽略。
    • 如果你将函数作为 initialState 传递,它将被视为一个初始化函数。它应该是纯净的,不应接受任何参数,并且应该返回任何类型的返回值。React将在初始化组件时调用你的初始化函数,并将它的返回值存储为初始状态。请参见下面的示例。

返回值

useState 返回一个恰好包含两个值的数组

  1. 当前状态。在第一次渲染期间,它将与您传入的 initialState 一致。
  2. set 函数 允许您将状态更新为不同的值并触发重新渲染。

注意事项

  • useState 是一个 Hook,因此您只能在组件的 顶级 或您自己的 Hook 中调用它。您不能在循环或条件内调用它。如果您需要这样做,请提取一个新的组件并将状态移入其中。
  • 在严格模式下,React 将 调用您的初始化函数两次帮助您查找意外的 impure 函数。 这只在开发环境中有效,不会影响生产环境。如果您的初始化函数是纯函数(应该如此),则这不会影响行为。其中一个调用的结果将被忽略。

set 函数,例如 setSomething(nextState)

useState 返回的 set 函数允许您将状态更新为不同的值并触发重新渲染。您可以直接传递下一个状态,或传递一个根据先前状态计算下一个状态的函数。

const [name, setName] = useState('Edward');

function handleClick() {
setName('Taylor');
setAge(a => a + 1);
// ...

参数

  • nextState:您希望状态具有的值。它可以是任何类型的值,但函数具有特殊行为。
    • 如果您将函数作为 nextState 传递,它将被视为一个 *更新函数*。它必须是纯函数,应该将挂起的 state 作为其唯一参数,并应该返回下一个 state。React 将您的更新函数放入队列中并重新渲染您的组件。在下次渲染期间,React 将通过将所有排队的更新器应用于先前状态来计算下一个状态。请参见下面的示例。

返回值

set 函数没有返回值。

注意事项

  • set 函数 只会在下次渲染时更新状态变量。如果您在调用 set 函数后读取状态变量,您仍然会得到之前显示在屏幕上的旧值

  • 如果根据 Object.is 比较确定您提供的新值与当前 state 相同,则 React 将 跳过重新渲染组件及其子组件。这是一个优化。尽管在某些情况下 React 仍然可能需要在跳过子组件之前调用您的组件,但这不应影响您的代码。

  • React 批量更新状态。它在 所有事件处理程序运行完毕 并调用其 set 函数后更新屏幕。这可以防止在单个事件期间多次重新渲染。如果您需要强制 React 更早更新屏幕(例如为了访问 DOM),可以使用 flushSync

  • set 函数具有稳定的标识,因此您经常会看到它从 Effect 依赖项中省略,但包含它不会导致 Effect 触发。如果 linter 允许您在没有错误的情况下省略依赖项,则可以安全地这样做。了解有关删除 Effect 依赖项的更多信息。

  • 在渲染期间调用 set 函数仅允许在当前正在渲染的组件中进行。React 将丢弃其输出并立即尝试使用新状态重新渲染它。这种模式很少需要,但您可以使用它来 存储来自先前渲染的信息请参见下面的示例。

  • 在严格模式下,React 将 调用您的更新函数两次帮助您查找意外的 impure 函数。 这只在开发环境中有效,不会影响生产环境。如果您的更新函数是纯函数(应该如此),则这不会影响行为。其中一个调用的结果将被忽略。


用法

向组件添加状态

在组件的顶层调用 useState 来声明一个或多个状态变量。

import { useState } from 'react';

function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...

约定是用 数组解构 将状态变量命名为 [something, setSomething]

useState 返回一个恰好包含两项的数组

  1. 这个状态变量的当前状态,最初设置为您提供的初始状态
  2. 用于更改状态的set 函数,您可以响应交互将其更改为任何其他值。

要更新屏幕上的内容,请使用某个新状态调用set 函数

function handleClick() {
setName('Robin');
}

React 将存储新状态,使用新值重新渲染您的组件,并更新 UI。

陷阱

调用set 函数不会更改已经在执行的代码中的当前状态

function handleClick() {
setName('Robin');
console.log(name); // Still "Taylor"!
}

它只会影响从下一次渲染开始useState 将返回的内容。

基本的 useState 示例

示例 1 4:
计数器(数字)

在这个例子中,count 状态变量保存一个数字。点击按钮会递增它。

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      You pressed me {count} times
    </button>
  );
}


基于先前状态更新状态

假设age42。此处理程序三次调用setAge(age + 1)

function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}

但是,单击一次后,age 将仅为 43 而不是 45!这是因为调用set 函数不会更新已经在运行的代码中的age 状态变量。因此,每个setAge(age + 1) 调用都变成setAge(43)

要解决此问题,可以将更新函数传递给setAge,而不是传递新状态。

function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}

这里,a => a + 1 是您的更新函数。它接收挂起状态并从中计算新状态

React 将您的更新函数放入队列中。然后,在下一次渲染期间,它将按相同的顺序调用它们。

  1. a => a + 1 将接收42 作为挂起状态,并返回43 作为新状态。
  2. a => a + 1 将接收43 作为挂起状态,并返回44 作为新状态。
  3. a => a + 1 将接收44 作为挂起状态,并返回45 作为新状态。

没有其他排队的更新,因此 React 最终将45 存储为当前状态。

按照惯例,通常将挂起状态参数命名为状态变量名称的首字母,例如agea。但是,您也可以将其命名为prevAge 或您认为更清晰的名称。

React 可能会在开发中两次调用您的更新程序以验证它们是否纯净的。

深入探讨

是否总是首选使用更新程序?

你可能会听到这样的建议:如果要设置的状态是根据之前的状态计算出来的,那么始终像这样编写代码 setAge(a => a + 1)。这样做并没有坏处,但并非总是必要的。

在大多数情况下,这两种方法之间没有区别。React 总是确保对于诸如点击之类的有意图的用户操作,age 状态变量会在下一次点击之前更新。这意味着点击处理程序在事件处理程序开始时看到“过时”的 age 的风险不存在。

但是,如果你在同一个事件中进行多次更新,那么更新器会很有帮助。如果你访问状态变量本身不方便(在优化重新渲染时可能会遇到这种情况),更新器也很有用。

如果你更喜欢一致性而不是稍微冗长的语法,那么如果要设置的状态是根据之前的状态计算出来的,那么始终编写一个更新器是合理的。如果它是根据某些 *其他* 状态变量的先前状态计算出来的,你可能需要将它们组合成一个对象并 使用 reducer

传递更新器和直接传递下一个状态之间的区别

示例 1 2:
传递更新器函数

此示例传递更新器函数,因此“+3”按钮有效。

import { useState } from 'react';

export default function Counter() {
  const [age, setAge] = useState(42);

  function increment() {
    setAge(a => a + 1);
  }

  return (
    <>
      <h1>Your age: {age}</h1>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <button onClick={() => {
        increment();
      }}>+1</button>
    </>
  );
}


更新状态中的对象和数组

你可以将对象和数组放入状态中。在 React 中,状态被认为是只读的,因此 你应该 *替换* 它,而不是 *修改* 你现有的对象。例如,如果你在状态中有一个 form 对象,不要修改它

// 🚩 Don't mutate an object in state like this:
form.firstName = 'Taylor';

相反,通过创建一个新的对象来替换整个对象

// ✅ Replace state with a new object
setForm({
...form,
firstName: 'Taylor'
});

阅读 更新状态中的对象更新状态中的数组 以了解更多信息。

状态中对象和数组的示例

示例 1 4:
表单 (对象)

在此示例中,form 状态变量保存一个对象。每个输入都有一个更改处理程序,它使用整个表单的下一个状态来调用 setForm{ ...form } 展开语法确保替换状态对象而不是修改它。

import { useState } from 'react';

export default function Form() {
  const [form, setForm] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: '[email protected]',
  });

  return (
    <>
      <label>
        First name:
        <input
          value={form.firstName}
          onChange={e => {
            setForm({
              ...form,
              firstName: e.target.value
            });
          }}
        />
      </label>
      <label>
        Last name:
        <input
          value={form.lastName}
          onChange={e => {
            setForm({
              ...form,
              lastName: e.target.value
            });
          }}
        />
      </label>
      <label>
        Email:
        <input
          value={form.email}
          onChange={e => {
            setForm({
              ...form,
              email: e.target.value
            });
          }}
        />
      </label>
      <p>
        {form.firstName}{' '}
        {form.lastName}{' '}
        ({form.email})
      </p>
    </>
  );
}


避免重新创建初始状态

React 只保存一次初始状态,并在接下来的渲染中忽略它。

function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...

尽管 createInitialTodos() 的结果仅用于初始渲染,但你仍然在每次渲染时都调用此函数。如果它正在创建大型数组或执行昂贵的计算,这可能会造成浪费。

为了解决这个问题,你可以 将其作为 *初始化器* 函数 传递给 useState

function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...

请注意,你传递的是 createInitialTodos(即函数本身),而不是 createInitialTodos()(即调用它的结果)。如果你将函数传递给 useState,React 只会在初始化期间调用它。

React 可能会 在开发中调用你的初始化器两次 以验证它们是否 纯净的。

传递初始化器和直接传递初始状态之间的区别

示例 1 2:
传递初始化函数

此示例传递初始化函数,因此createInitialTodos函数仅在初始化期间运行。当组件重新渲染时(例如,当您在输入框中键入内容时),它不会运行。

import { useState } from 'react';

function createInitialTodos() {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: 'Item ' + (i + 1)
    });
  }
  return initialTodos;
}

export default function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos);
  const [text, setText] = useState('');

  return (
    <>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        setTodos([{
          id: todos.length,
          text: text
        }, ...todos]);
      }}>Add</button>
      <ul>
        {todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}


使用键重置状态

渲染列表时,您经常会遇到key属性。但是,它还有另一个用途。

您可以通过向组件传递不同的key来重置组件的状态。在此示例中,“重置”按钮更改了version状态变量,我们将它作为key传递给Form。当key更改时,React会从头开始重新创建Form组件(及其所有子组件),因此其状态将被重置。

阅读保留和重置状态以了解更多信息。

import { useState } from 'react';

export default function App() {
  const [version, setVersion] = useState(0);

  function handleReset() {
    setVersion(version + 1);
  }

  return (
    <>
      <button onClick={handleReset}>Reset</button>
      <Form key={version} />
    </>
  );
}

function Form() {
  const [name, setName] = useState('Taylor');

  return (
    <>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <p>Hello, {name}.</p>
    </>
  );
}


存储来自先前渲染的信息

通常,您会在事件处理程序中更新状态。但是,在极少数情况下,您可能希望根据渲染来调整状态——例如,当道具更改时,您可能希望更改状态变量。

在大多数情况下,您不需要这样做

如果以上方法都不适用(这种情况很少见),则可以使用一种模式来根据迄今为止已渲染的值更新状态,方法是在组件渲染时调用set函数。

这是一个示例。CountLabel组件显示传递给它的count道具

export default function CountLabel({ count }) {
return <h1>{count}</h1>
}

假设您想显示自上次更改以来计数器是增加还是减少count道具不会告诉您这一点——您需要跟踪其先前的值。添加prevCount状态变量来跟踪它。添加另一个名为trend的状态变量来保存计数是增加还是减少。比较prevCountcount,如果它们不相等,则更新prevCounttrend。现在您可以同时显示当前计数道具以及自上次渲染以来它如何变化

import { useState } from 'react';

export default function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'increasing' : 'decreasing');
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </>
  );
}

请注意,如果在渲染期间调用set函数,则它必须位于像prevCount !== count这样的条件内,并且条件内必须有像setPrevCount(count)这样的调用。否则,您的组件将循环重新渲染,直到崩溃。此外,您只能以此方式更新当前正在渲染的组件的状态。在渲染期间调用另一个组件的set函数是一个错误。最后,您的set调用仍然应该在不发生变异的情况下更新状态——这并不意味着您可以违反纯函数的其他规则。

这种模式可能难以理解,通常最好避免。但是,它比在 effect 中更新状态更好。当您在渲染期间调用set函数时,React会在您的组件使用return语句退出之后,以及渲染子组件之前立即重新渲染该组件。这样,子组件不需要重新渲染两次。您的组件函数的其余部分仍然会执行(结果将被丢弃)。如果您的条件位于所有 Hook 调用的下方,您可以添加一个提前的return;来更早地重新启动渲染。


故障排除

我更新了状态,但是日志显示的是旧值

调用 set 函数不会改变正在运行的代码中的状态

function handleClick() {
console.log(count); // 0

setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!

setTimeout(() => {
console.log(count); // Also 0!
}, 5000);
}

这是因为 状态的行为类似于快照。 更新状态请求使用新状态值进行另一次渲染,但不会影响您已运行的事件处理程序中的 count JavaScript 变量。

如果您需要使用下一个状态,可以在将其传递给 set 函数之前将其保存在变量中

const nextCount = count + 1;
setCount(nextCount);

console.log(count); // 0
console.log(nextCount); // 1

我更新了状态,但屏幕没有更新

React 将忽略您的更新,如果下一个状态等于上一个状态,这由 Object.is 比较确定。当您直接更改状态中的对象或数组时,通常会发生这种情况

obj.x = 10; // 🚩 Wrong: mutating existing object
setObj(obj); // 🚩 Doesn't do anything

您更改了现有的 obj 对象并将其传回 setObj,因此 React 忽略了更新。要解决此问题,您需要确保始终替换状态中的对象和数组,而不是更改它们

// ✅ Correct: creating a new object
setObj({
...obj,
x: 10
});

我收到错误:“重新渲染次数过多”

您可能会收到一条错误消息:重新渲染次数过多。React 限制了渲染次数以防止无限循环。 通常,这意味着您在渲染期间无条件地设置状态,因此您的组件进入循环:渲染、设置状态(这会导致渲染)、渲染、设置状态(这会导致渲染),依此类推。这通常是由于指定事件处理程序时出错造成的

// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>

// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>

// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>

如果您找不到此错误的原因,请单击控制台中错误旁边的箭头,并查看 JavaScript 堆栈以查找导致此错误的特定 set 函数调用。


我的初始化程序或更新程序函数运行了两次

严格模式下,React 将调用您的某些函数两次而不是一次

function TodoList() {
// This component function will run twice for every render.

const [todos, setTodos] = useState(() => {
// This initializer function will run twice during initialization.
return createTodos();
});

function handleClick() {
setTodos(prevTodos => {
// This updater function will run twice for every click.
return [...prevTodos, createTodo()];
});
}
// ...

这是预期的行为,不应破坏您的代码。

仅限开发环境的行为可帮助您保持组件的纯净性。 React 使用其中一个调用的结果,并忽略另一个调用的结果。只要您的组件、初始化程序和更新程序函数是纯函数,这就不应该影响您的逻辑。但是,如果它们意外地是非纯函数,这将帮助您注意到错误。

例如,此非纯更新程序函数更改了状态中的数组

setTodos(prevTodos => {
// 🚩 Mistake: mutating state
prevTodos.push(createTodo());
});

因为 React 调用您的更新程序函数两次,您将看到待办事项添加了两次,因此您将知道存在错误。在此示例中,您可以通过替换数组而不是更改它来修复错误

setTodos(prevTodos => {
// ✅ Correct: replacing with new state
return [...prevTodos, createTodo()];
});

现在此更新程序函数是纯函数,额外调用它一次不会改变行为。这就是为什么 React 调用它两次可以帮助您发现错误的原因。只有组件、初始化程序和更新程序函数需要是纯函数。事件处理程序不需要是纯函数,因此 React 永远不会调用您的事件处理程序两次。

阅读 保持组件纯净 以了解更多信息。


我尝试将状态设置为函数,但它却被调用了

您不能像这样将函数放入状态

const [fn, setFn] = useState(someFunction);

function handleClick() {
setFn(someOtherFunction);
}

因为您正在传递一个函数,React 假设 someFunction 是一个初始化函数,并且 someOtherFunction 是一个更新函数,因此它尝试调用它们并存储结果。要实际存储函数,您必须在两种情况下都在它们之前加上 () =>。然后 React 将存储您传递的函数。

const [fn, setFn] = useState(() => someFunction);

function handleClick() {
setFn(() => someOtherFunction);
}