本专栏致力于每周分享一个项目,如果本文对你有帮助的话,欢迎点赞或者关注☘️
React18 源码系列会随着学习 React 源码的实时进度而实时更新:约,两天一小改,五天一大改。
useState是什么
useState是一个 React Hook,可让您向组件添加状态变量。
const [state, setState] = useState(initialState)
在组件顶层调用useState来声明状态变量。约定是使用数组解构来命名状态变量,例如[something, setSomething]
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [todos, setTodos] = useState(() => createTodos());
// ...
参数:
- initialState:您希望状态初始的值。它可以是任何类型的值。但函数有特殊的行为。初始渲染后该参数将被忽略。如果将函数作为
initialState传递,它将被视为初始化函数。它应该是纯的,不应该接受任何参数,并且应该返回任何类型的值。 React 将在初始化组件时调用您的初始化函数,并将其返回值存储为初始状态 - return:
useState返回一个包含两个值的数组: - 目前的state:在第一次渲染期间,它将匹配您传递的
initialState。 [set](https://react.dev/reference/react/useState#setstate)函数可让您将状态更新为不同的值并触发重新渲染。
注意事项
useState是一个 Hook,因此您只能在组件的顶层或您自己的 Hook 中调用它。您不能在循环或条件内调用它。如果需要,请提取一个新组件并将state移入其中。
Set Function
useState返回的set函数允许您将状态更新为不同的值并触发重新渲染。您可以直接传递下一个状态,或者根据前一个状态计算它的函数:
const [name, setName] = useState('Edward');
function handleClick() {
setName('Taylor');
setAge(a => a + 1);
// ...
参数:
nextState:您想要的状态值。它可以是任何类型的值,但函数有特殊的行为- 如果您将函数作为
nextState传递,它将被视为更新程序函数。它必须是纯粹的,应该将挂起状态作为唯一的参数,并且应该返回下一个状态。 React 会将您的更新程序函数放入队列中并重新渲染您的组件。在下一次渲染期间,React 将通过将所有排队的更新程序应用到前一个状态来计算下一个状态。 - return
set函数没有返回值。
注意事项:
set函数仅为下一次渲染更新状态变量。如果您在调用set函数后读取状态变量,您仍然会获得调用之前屏幕上的旧值。
如果您提供的新值与当前state相同(通过[Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)比较确定),React 将跳过重新渲染组件及其子组件。 这是一个优化。
React批量状态更新。在所有事件处理程序运行并调用其set的函数后,它会更新屏幕。这可以防止在单个事件期间多次重新渲染。在极少数情况下,您需要强制 React 提前更新屏幕,例如访问 DOM,您可以使用[flushSync](https://react.dev/reference/react-dom/flushSync)。
set函数具有稳定的标识,因此您经常会看到它从 Effect 依赖项中省略,但包含它不会导致 Effect 触发。如果 linter 允许您省略依赖项而不会出现错误,那么这样做是安全的。
渲染期间仅允许在当前渲染组件内调用set函数。 React 将丢弃其输出并立即尝试使用新状态再次渲染它。这种模式很少需要,但您可以使用它来存储先前渲染的信息。
用法
向组件添加状态
在组件顶层调用useState来声明一个或多个状态变量。约定是使用数组解构来命名状态变量,例如[something, setSomething]
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...
useState返回一个包含两个项目的数组:
- 该状态变量的当前状态,最初设置为您提供的初始状态。
set函数可让您将其更改为任何其他值以响应交互。
要更新屏幕上的内容,请使用以下状态调用set函数:React 将存储下一个状态,使用新值再次渲染组件,并更新 UI。
function handleClick() {
setName('Robin');
}
注意事项:
调用set函数**不会**更改已执行代码中的当前状态:它只影响useState从下一次渲染开始返回的内容。
function handleClick() {
setName('Robin');
console.log(name); // Still "Taylor"!
}
您可以在同一组件中声明多个状态变量。每个状态变量都是完全独立的。
根据之前的状态更新状态
假设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而不是下一个状态:这里, a => a + 1是您的更新函数。它获取待处理状态并从中计算下一个状态。
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
React 将您的更新器函数放入队列中。然后,在下一次渲染期间,它将以相同的顺序调用它们:
a => a + 1将接收42作为待处理状态并返回43作为下一个状态a => a + 1将接收43作为待处理状态并返回44作为下一个状态。a => a + 1将接收44作为待处理状态并返回45作为下一个状态。
使用更新程序函数总是首选吗
如果您设置的状态是根据先前的状态计算的,您可能会听到建议始终编写类似setAge(a => a + 1)的代码。这没有什么坏处,但也并不总是必要的。
那么如果您设置的状态是根据以前的状态计算的,则始终编写更新程序是合理的。
如果它是根据其他状态变量的先前状态计算的,您可能希望将它们组合成一个对象并使用reducer
更新状态中的对象和数组
您可以将对象和数组放入状态。在 React 中,状态被认为是只读的,因此您应该替换它而不是改变现有的对象。例如,如果您有一个处于状态的form对象,请不要改变它:
// 🚩 Don't mutate an object in state like this:
form.firstName = 'Taylor';
相反,通过创建一个新对象来替换整个对象:
// ✅ Replace state with a new object
setForm({
...form,
firstName: 'Taylor'
});
传递初始化器和直接传递初始状态之间的区别
此示例传递了初始化函数,因此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>
</>
);
}
此示例不传递初始化函数,因此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属性。然而,它还有另一个目的.
您可以通过将不同的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](https://react.dev/reference/react/useMemo)Hook可以提供帮助。 - 如果您想重置整个组件树的状态,请将不同的
[key](https://react.dev/reference/react/useState#resetting-state-with-a-key)传递给您的组件。 - 如果可以的话,更新事件处理程序中的所有相关状态。
在极少数情况下,这些都不适用,您可以使用一种模式,通过在组件渲染时调用set函数,根据目前已渲染的值来更新状态。
假设您想显示自上次更改以来计数器是增加还是减少。 count属性不会告诉你这一点——你需要跟踪它以前的值。添加prevCount状态变量来跟踪它。添加另一个称为trend的状态变量来保存计数是增加还是减少。将prevCount与count进行比较,如果它们不相等,则更新prevCount和trend 。现在您可以显示当前的 count 属性以及它自上次渲染以来的变化情况。
请注意,如果在渲染时调用set函数,则它必须位于prevCount !== count类的条件内,并且条件内必须有setPrevCount(count)之类的调用。否则,您的组件将循环重新渲染,直到崩溃。另外,您只能像这样更新当前渲染组件的状态。在渲染过程中调用另一个组件的set函数是错误的。最后,您的set调用仍应更新状态而不发生突变- 这并不意味着您可以打破纯函数的其他规则。
这种模式可能很难理解,通常最好避免。然而,它比在在Effect上更新状态要好。当您在渲染期间调用set函数时,React 将在组件使用return语句退出后、渲染子组件之前立即重新渲染该组件。这样,孩子们就不需要渲染两次。组件函数的其余部分仍然会执行(并且结果将被丢弃)。如果您的条件低于所有 Hook 调用,您可以添加提前return;提前重新开始渲染。
export default function CountLabel({ count }) {
return <h1>{count}</h1>
}
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函数不会更改正在运行的代码中的状态:
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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
});
我收到错误:“重新渲染次数过多”
您可能会收到一条错误消息: Too many re-renders. React limits the number of renders to prevent an infinite loop. 通常,这意味着您在 render 期间无条件设置状态,因此您的组件进入循环:渲染、设置状态(导致渲染)、渲染、设置状态(导致渲染)等等。通常,这是由于指定事件处理程序时出现错误造成的:
// 🚩 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>
我的初始化程序或更新程序函数运行两次
只有组件、初始化器和更新器函数需要是纯的。 事件处理程序不需要是纯粹的,因此 React 永远不会两次调用您的事件处理程序。
setTodos(prevTodos => {
// 🚩 Mistake: mutating state
prevTodos.push(createTodo());
});
setTodos(prevTodos => {
// ✅ Correct: replacing with new state
return [...prevTodos, createTodo()];
});
我试图将状态设置为函数,但它被调用
您不能将函数置于这样的状态:
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}
因为你传递的是一个函数,React 假设someFunction是一个初始化函数,而someOtherFunction是一个更新函数,所以它尝试调用它们并存储结果。要实际存储函数,在这两种情况下都必须将() =>放在它们之前。然后 React 会存储你传递的函数。
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}
参考链接
- react学习资源:
- react.dev/reference/r…
关于作者
作者:Wandra
内容:算法 | 趋势 |源码|Vue | React | CSS | Typescript | Webpack | Vite | GithubAction | GraphQL | Uniqpp。
专栏:欢迎关注呀🌹
本专栏致力于每周分享一个项目,如果本文对你有帮助的话,欢迎点赞或者关注☘️