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
返回一个包含两个值的数组
- 当前状态。在第一次渲染期间,它将与您传递的
initialState
匹配。 - 允许您将状态更新为不同值并触发重新渲染的
set
函数。
注意事项
useState
是一个 Hook,所以你只能在组件的顶层或你自己的 Hooks 中调用它。你不能在循环或条件语句中调用它。如果你需要这样做,请提取一个新的组件并将状态移入其中。- 在严格模式下,React 会调用你的初始化函数两次,以便帮助你发现意外的副作用。这是仅在开发环境下的行为,不会影响生产环境。如果你的初始化函数是纯函数(它应该是),这应该不会影响行为。其中一次调用的结果将被忽略。
set
函数,例如 setSomething(nextState)
由 useState
返回的 set
函数允许你将状态更新为不同的值并触发重新渲染。你可以直接传递下一个状态,或者传递一个根据先前状态计算状态的函数。
const [name, setName] = useState('Edward');
function handleClick() {
setName('Taylor');
setAge(a => a + 1);
// ...
参数
nextState
:你希望状态的值。它可以是任何类型的值,但函数有一种特殊行为。- 如果你传递一个函数作为
nextState
,它将被视为一个_更新函数_。它必须是纯函数,应该将待定状态作为其唯一参数,并且应该返回下一个状态。React 会将你的更新函数放入队列并重新渲染你的组件。在下一次渲染期间,React 将通过将所有排队的更新器应用于先前状态来计算下一个状态。请参阅下面的示例。
- 如果你传递一个函数作为
返回值
set
函数没有返回值。
注意事项
-
set
函数仅更新_下一次_渲染的状态变量。如果你在调用set
函数后读取状态变量,你仍然会得到调用之前的旧值。 -
如果通过
Object.is
比较确定你提供的新值与当前state
相同,React 将跳过重新渲染组件及其子组件。这是一种优化。尽管在某些情况下 React 可能仍然需要在跳过子组件之前调用你的组件,但这不应该影响你的代码。 -
React 批量处理状态更新。它会在所有事件处理程序都运行完毕并调用了它们的
set
函数_之后_更新屏幕。这可以防止在单个事件期间进行多次重新渲染。在极少数情况下,你需要强制 React 更早地更新屏幕(例如访问 DOM),你可以使用flushSync
。 -
_渲染期间_调用
set
函数只允许从当前正在渲染的组件中进行。React 将丢弃其输出并立即尝试使用新状态再次渲染它。这种模式很少需要,但你可以使用它来存储先前渲染的信息。请参阅下面的示例。 -
在严格模式下,React 会调用你的更新函数两次,以便帮助你发现意外的副作用。这是仅在开发环境下的行为,不会影响生产环境。如果你的更新函数是纯函数(它应该是),这应该不会影响行为。其中一次调用的结果将被忽略。
用法
向组件添加状态
在组件的顶层调用 useState
来声明一个或多个 状态变量。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...
惯例是使用 数组解构将状态变量命名为 [something, setSomething]
。
useState
返回一个包含两个元素的数组
- 此状态变量的 当前状态,最初设置为您提供的 初始状态。
- 允许您根据交互将其更改为任何其他值的
set
函数。
要更新屏幕上的内容,请使用下一个状态调用 set
函数
function handleClick() {
setName('Robin');
}
React 将存储下一个状态,使用新值再次渲染您的组件,并更新 UI。
根据先前的状态更新状态
假设 age
是 42
。此处理程序调用了三次 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 将您的更新器函数放入 队列 中。然后,在下一次渲染期间,它将按相同的顺序调用它们
a => a + 1
将接收42
作为待处理状态,并返回43
作为下一个状态。a => a + 1
将接收43
作为待处理状态,并返回44
作为下一个状态。a => a + 1
将接收44
作为待处理状态,并返回45
作为下一个状态。
没有其他排队的更新,因此 React 最终会将 45
存储为当前状态。
按照惯例,通常使用状态变量名称的首字母来命名待处理状态参数,例如 age
使用 a
。但是,您也可以将其称为 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> </> ); }
存储来自先前渲染的信息
通常,你会在事件处理程序中更新状态。但是,在极少数情况下,你可能希望根据渲染来调整状态——例如,你可能希望在 prop 更改时更改状态变量。
在大多数情况下,你不需要这样做
- **如果所需的值可以完全从当前 props 或其他状态计算出来,请完全删除冗余状态。** 如果你担心计算次数过多,
useMemo
钩子可以提供帮助。 - 如果要重置整个组件树的状态,请向组件传递不同的
key
。 - 如果可以,请在事件处理程序中更新所有相关的状态。
在极少数情况下,如果以上方法都不适用,则可以使用一种模式来根据已渲染的值更新状态,方法是在组件渲染时调用 set
函数。
下面是一个例子。 这个CountLabel
组件显示传递给它的count
属性
export default function CountLabel({ count }) {
return <h1>{count}</h1>
}
假设你想显示计数器自上次更改后是*增加还是减少*了。count
属性不会告诉你这一点——你需要跟踪它的先前值。添加prevCount
状态变量来跟踪它。添加另一个名为 trend
的状态变量来保存计数是增加还是减少。比较prevCount
和 count
,如果它们不相等,则同时更新prevCount
和 trend
。现在,你可以同时显示当前的 count 属性以及*它自上次渲染后是如何变化的*。
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
我已经更新了状态,但屏幕没有更新
如果下一个状态等于先前状态(通过 Object.is
比较确定),React 将 忽略您的更新。 这通常发生在您直接更改状态中的对象或数组时
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);
}