useState 是一个 React Hook,可让你向组件添加一个 state 变量。
const [state, setState] = useState(initialState)参考
useState(initialState)
在组件顶层调用 useState 来声明一个 state 变量。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [todos, setTodos] = useState(() => createTodos());
// ...按照约定,state 变量通常命名为 [something, setSomething],并使用 数组解构。
参数
initialState:你希望 state 的初始值。它可以是任何类型的值,但对于函数有特殊行为。这个参数在初次渲染后会被忽略。- 如果你把一个函数作为
initialState传入,它会被当作一个 initializer function。它应该是纯函数,不应接受任何参数,并且应返回任意类型的值。React 会在初始化组件时调用你的 initializer function,并将其返回值作为初始 state 存储起来。查看下面的示例。
- 如果你把一个函数作为
返回值
useState 返回一个恰好包含两个值的数组:
- 当前的 state。在第一次渲染时,它将与你传入的
initialState相匹配。 - 允许你将 state 更新为其他值并触发重新渲染的
set函数。
注意事项
useState是一个 Hook,因此你只能在组件顶层或你自己的 Hooks 中调用它。你不能在循环或条件中调用它。如果你需要这样做,请提取一个新组件并把 state 移到其中。- 在 Strict Mode 下,React 会调用你的 initializer function 两次,以帮助你发现意外的副作用。 这是仅开发环境下的行为,不会影响生产环境。如果你的 initializer function 是纯函数(它本来就应该是),这不会影响行为。其中一次调用的结果会被忽略。
set 函数,例如 setSomething(nextState)
useState 返回的 set 函数允许你将 state 更新为不同的值并触发重新渲染。你可以直接传入下一个 state,或者传入一个根据前一个 state 计算出它的函数:
const [name, setName] = useState('Edward');
function handleClick() {
setName('Taylor');
setAge(a => a + 1);
// ...参数
nextState:你希望 state 变成的值。它可以是任何类型的值,但对于函数有特殊行为。- 如果你把一个函数作为
nextState传入,它会被当作一个 updater function。它必须是纯函数,只应接收待处理的 state 作为唯一参数,并应返回下一个 state。React 会把你的 updater function 放入队列并重新渲染你的组件。在下一次渲染时,React 会通过把队列中的所有 updater 依次应用到前一个 state 上来计算下一个 state。查看下面的示例。
- 如果你把一个函数作为
返回值
set 函数没有返回值。
注意事项
-
set函数只会更新下一次渲染的 state 变量。如果你在调用set函数之后读取 state 变量,你仍然会得到旧值,也就是你调用之前屏幕上显示的值。 -
如果你提供的新值与当前
state完全相同(根据Object.is比较判断),React 会跳过重新渲染该组件及其子组件。 这是一个优化。虽然在某些情况下,React 可能仍然需要在跳过子组件之前调用你的组件,但这不应该影响你的代码。 -
React 会批量处理 state 更新。 它会在所有事件处理函数运行完并调用了它们的
set函数之后再更新屏幕。这可以防止在单个事件中发生多次重新渲染。在极少数情况下,如果你需要强制 React 更早地更新屏幕,例如为了访问 DOM,你可以使用flushSync。 -
set函数具有稳定的身份,因此你经常会看到它被省略在 Effect 依赖项之外,但即使包含它也不会导致 Effect 触发。如果 linter 允许你在不报错的情况下省略某个依赖项,那么这样做是安全的。了解更多关于移除 Effect 依赖项的内容。 -
在渲染期间调用
set函数只允许在当前正在渲染的组件内部进行。React 会丢弃其输出,并立即尝试使用新 state 重新渲染它。这种模式很少需要,但你可以用它来存储上一次渲染中的信息。查看下面的示例。 -
在 Strict Mode 下,React 会调用你的 updater function 两次,以帮助你发现意外的副作用。 这是仅开发环境下的行为,不会影响生产环境。如果你的 updater function 是纯函数(它本来就应该是),这不会影响行为。其中一次调用的结果会被忽略。
使用
向组件添加 state
在组件顶层调用 useState,以声明一个或多个 state 变量。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...按照约定,state 变量通常命名为 [something, setSomething],并使用 数组解构。
useState 返回一个恰好包含两个项的数组:
- 这个 state 变量的 当前 state,初始值为你提供的 初始 state。
- 允许你根据交互将其更改为其他值的
set函数。
要更新屏幕上的内容,请用某个下一个 state 调用 set 函数:
function handleClick() {
setName('Robin');
}React 会保存下一个 state,再次使用新值渲染你的组件,并更新 UI。
Example 1 of 4: 计数器(数字)
在这个示例中,count state 变量保存一个数字。点击按钮会使其递增。
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> ); }
基于前一个 state 更新 state
假设 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 state 变量。所以每次 setAge(age + 1) 调用都会变成 setAge(43)。
要解决这个问题,你可以向 setAge 传入一个 updater function,而不是下一个 state:
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 就是你的 updater function。它接收 待处理的 state,并基于它计算出 下一个 state。
React 会把你的 updater functions 放入队列。 然后在下一次渲染时按相同顺序调用它们:
a => a + 1会接收42作为待处理的 state,并返回43作为下一个 state。a => a + 1会接收43作为待处理的 state,并返回44作为下一个 state。a => a + 1会接收44作为待处理的 state,并返回45作为下一个 state。
队列中没有其他更新,因此 React 最终会将 45 保存为当前 state。
按照约定,通常会用 state 变量名称的第一个字母来命名待处理的 state 参数,例如用 a 表示 age。不过,你也可以把它命名为 prevAge 或其他你觉得更清晰的名字。
React 可能会在开发环境中调用你的 updaters 两次,以验证它们是否纯函数。
Deep Dive
你可能会听到这样的建议:如果你设置的 state 是从前一个 state 计算出来的,就总是写成 setAge(a => a + 1)。这样写没有坏处,但也并不总是必要。
在大多数情况下,这两种方式没有区别。React 总是会确保,对于点击这类有意的用户操作,age state 变量会在下一次点击之前更新。这意味着,点击处理函数在事件开始时看到“过期”的 age 并不会有风险。
不过,如果你在同一个事件中执行多次更新,updater 会很有帮助。如果直接访问 state 变量本身不方便,它们也很有用(在优化重新渲染时你可能会遇到这种情况)。
如果你更偏好一致性而不是稍微更啰嗦的语法,那么在 state 是由前一个 state 计算出来时,总是写 updater 是合理的。如果它是由某个其他 state 变量的前一个 state 计算出来的,你可能会想把它们合并成一个对象,并使用 reducer。
Example 1 of 2: 传入 updater function
这个示例传入了 updater function,因此 “+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> </> ); }
更新 state 中的对象和数组
你可以把对象和数组放入 state 中。在 React 中,state 被视为只读,因此你应该替换它,而不是修改现有对象。例如,如果你在 state 中有一个 form 对象,不要直接修改它:
// 🚩 不要像这样修改 state 中的对象:
form.firstName = 'Taylor';相反,应该通过创建一个新对象来替换整个对象:
// ✅ 用一个新对象替换 state
setForm({
...form,
firstName: 'Taylor'
});阅读 更新 state 中的对象 和 更新 state 中的数组 以了解更多。
Example 1 of 4: 表单(对象)
在这个示例中,form state 变量保存一个对象。每个输入框都有一个变更处理函数,它会用整个表单的下一个 state 调用 setForm。{ ...form } 扩展语法确保 state 对象被替换而不是被修改。
import { useState } from 'react'; export default function Form() { const [form, setForm] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }); return ( <> <label> 名字: <input value={form.firstName} onChange={e => { setForm({ ...form, firstName: e.target.value }); }} /> </label> <label> 姓氏: <input value={form.lastName} onChange={e => { setForm({ ...form, lastName: e.target.value }); }} /> </label> <label> 邮箱: <input value={form.email} onChange={e => { setForm({ ...form, email: e.target.value }); }} /> </label> <p> {form.firstName}{' '} {form.lastName}{' '} ({form.email}) </p> </> ); }
避免重新创建初始 state
React 只会保存一次初始 state,并在后续渲染中忽略它。
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...虽然 createInitialTodos() 的结果只会用于初次渲染,但你仍然在每次渲染时调用了这个函数。如果它创建的是大型数组或执行了昂贵的计算,这会很浪费。
为了解决这个问题,你可以改为把它作为一个 initializer 函数传给 useState:
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...注意,你传入的是 createInitialTodos,也就是函数本身,而不是 createInitialTodos(),即调用它的结果。如果你向 useState 传入一个函数,React 只会在初始化时调用它。
React 可能会在开发环境中调用你的 initializers 两次,以验证它们是否纯函数。
Example 1 of 2: 传入 initializer function
这个示例传入了 initializer function,因此 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 重置 state
你经常会在渲染列表时遇到 key 属性。不过,它还有另一个用途。
你可以通过向组件传递不同的 key 来重置组件的 state。 在这个示例中,Reset 按钮会更改 version state 变量,我们将它作为 key 传给 Form。当 key 改变时,React 会从头重新创建 Form 组件(以及它的所有子组件),因此它的 state 会被重置。
阅读 保留和重置 state 以了解更多。
import { useState } from 'react'; export default function App() { const [version, setVersion] = useState(0); function handleReset() { setVersion(version + 1); } return ( <> <button onClick={handleReset}>重置</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> </> ); }
存储上一次渲染中的信息
通常,你会在事件处理函数中更新 state。不过,在极少数情况下,你可能希望根据渲染来调整 state——例如,当 prop 改变时你可能想修改某个 state 变量。
在大多数情况下,你并不需要这样做:
- 如果你需要的值可以完全根据当前 props 或其他 state 计算出来,就把那个冗余 state 完全移除。 如果你担心重新计算太频繁,
useMemoHook 可以帮忙。 - 如果你想重置整个组件树的 state,给你的组件传入不同的
key。 - 如果可以的话,在事件处理函数中更新所有相关的 state。
在极少数这些都不适用的情况下,你可以使用一种模式:在组件渲染时调用 set 函数,根据到目前为止已经渲染出来的值来更新 state。
下面是一个示例。这个 CountLabel 组件显示传入的 count prop:
export default function CountLabel({ count }) {
return <h1>{count}</h1>
}假设你想显示计数器相较于上一次变化是增加还是减少。count prop 并不能告诉你这一点——你需要跟踪它的前一个值。添加 prevCount state 变量来跟踪它。再添加一个名为 trend 的 state 变量来保存计数是增加还是减少。将 prevCount 与 count 比较,如果它们不相等,就同时更新 prevCount 和 trend。现在你就可以同时显示当前的 count prop 以及它自上一次渲染以来发生了怎样的变化。
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) 的调用。否则,你的组件会陷入循环重新渲染直到崩溃。另外,你只能像这样更新当前正在渲染的组件的 state。在渲染期间调用另一个组件的 set 函数是错误的。最后,你的 set 调用仍然应该在不修改的情况下更新 state——这并不意味着你可以违反 纯函数。 的其他规则。
这种模式可能很难理解,通常最好避免。不过,它比在 effect 中更新 state 更好。当你在渲染期间调用 set 函数时,React 会在你的组件通过 return 语句退出之后、渲染子组件之前立即重新渲染该组件。这样,子组件就不需要渲染两次。你组件函数的其余部分仍然会执行(结果会被丢弃)。如果你的条件位于所有 Hook 调用之后,你可以添加一个提前的 return; 来更早地重新开始渲染。
故障排除
我已经更新了 state,但日志里拿到的还是旧值
调用 set 函数不会改变正在运行代码中的状态:
function handleClick() {
console.log(count); // 0
setCount(count + 1); // 请求用 1 重新渲染
console.log(count); // 仍然是 0!
setTimeout(() => {
console.log(count); // 还是 0!
}, 5000);
}这是因为 state 的行为就像一个快照。 更新 state 会请求使用新的 state 值进行另一次渲染,但不会影响你已经在运行中的事件处理函数里的 count JavaScript 变量。
如果你需要使用下一个 state,可以在传给 set 函数之前把它保存到一个变量里:
const nextCount = count + 1;
setCount(nextCount);
console.log(count); // 0
console.log(nextCount); // 1我已经更新了 state,但屏幕没有更新
如果下一个 state 和上一个 state 相等,React 会根据 Object.is 比较忽略你的更新。这通常发生在你直接修改 state 中的对象或数组时:
obj.x = 10; // 🚩 错误:直接修改现有对象
setObj(obj); // 🚩 什么也不会发生你修改了现有的 obj 对象,并把它传回给 setObj,所以 React 忽略了这次更新。要修复这个问题,你需要确保自己总是[替换] state 中的对象和数组,而不是[修改]它们:(#updating-objects-and-arrays-in-state)
// ✅ 正确:创建一个新对象
setObj({
...obj,
x: 10
});我遇到了一个错误:“Too many re-renders”
你可能会看到这样的错误:Too many re-renders. React limits the number of renders to prevent an infinite loop. 通常这意味着你在 渲染期间 无条件地设置了 state,于是你的组件进入了一个循环:渲染、设置 state(这会导致渲染)、渲染、设置 state(这会导致渲染),如此反复。很多时候,这是因为事件处理函数的写法有误:
// 🚩 错误:在渲染期间调用处理函数
return <button onClick={handleClick()}>Click me</button>
// ✅ 正确:传递事件处理函数
return <button onClick={handleClick}>Click me</button>
// ✅ 正确:传递内联函数
return <button onClick={(e) => handleClick(e)}>Click me</button>如果你找不到这个错误的原因,点击控制台中错误旁边的箭头,查看 JavaScript 调用栈,找到导致该错误的具体 set 函数调用。
我的初始化器或更新器函数运行了两次
在 严格模式 下,React 会把你的一些函数调用两次,而不是一次:
function TodoList() {
// 这个组件函数在每次渲染时都会运行两次。
const [todos, setTodos] = useState(() => {
// 这个初始化器函数在初始化期间会运行两次。
return createTodos();
});
function handleClick() {
setTodos(prevTodos => {
// 这个更新器函数在每次点击时都会运行两次。
return [...prevTodos, createTodo()];
});
}
// ...这是预期行为,不会破坏你的代码。
这种仅限开发环境的行为有助于你保持组件纯净。 React 会使用其中一次调用的结果,并忽略另一次调用的结果。只要你的组件、初始化器和更新器函数是纯函数,这就不会影响你的逻辑。不过,如果它们不小心变成了非纯函数,这能帮助你发现错误。
例如,这个非纯更新器函数会修改 state 中的数组:
setTodos(prevTodos => {
// 🚩 错误:修改 state
prevTodos.push(createTodo());
});因为 React 会把你的更新器函数调用两次,所以你会看到待办事项被添加了两次,这样你就知道这里有错误。在这个例子中,你可以通过替换数组而不是修改它来修复这个错误:
setTodos(prevTodos => {
// ✅ 正确:替换为新 state
return [...prevTodos, createTodo()];
});现在这个更新器函数是纯的了,额外调用一次也不会改变行为。这就是为什么 React 调用它两次能帮助你发现错误。只有组件、初始化器和更新器函数需要是纯的。 事件处理函数不需要是纯的,所以 React 绝不会把你的事件处理函数调用两次。
阅读保持组件纯净了解更多。
我想把 state 设置为一个函数,但它却被调用了
你不能像这样把一个函数放进 state 里:
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}因为你传递的是一个函数,React 会认为 someFunction 是一个初始化器函数,而 someOtherFunction 是一个更新器函数,所以它会尝试调用它们并存储结果。要真正存储一个函数,你必须在这两种情况下都在它们前面加上 () =>。这样 React 就会存储你传入的函数。
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}