状态在组件之间是隔离的。React 根据它们在 UI 树中的位置跟踪哪个状态属于哪个组件。你可以控制何时保留状态以及何时在重新渲染之间重置状态。
¥State is isolated between components. React keeps track of which state belongs to which component based on their place in the UI tree. You can control when to preserve state and when to reset it between re-renders.
你将学习到
-
React 何时选择保留或重置状态时
¥When React chooses to preserve or reset the state
-
如何强制 React 重置组件的状态
¥How to force React to reset component’s state
-
键和类型如何影响状态是否被保留
¥How keys and types affect whether the state is preserved
状态与渲染树中的位置相关联
¥State is tied to a position in the render tree
React 为 UI 中的组件结构构建 渲染树。
¥React builds render trees for the component structure in your UI.
当你给一个组件状态时,你可能会想到状态 “存在” 组件内部。但状态实际上保存在 React 内部。React 根据该组件在渲染树中的位置将其持有的每个状态与正确的组件关联起来。
¥When you give a component state, you might think the state “lives” inside the component. But the state is actually held inside React. React associates each piece of state it’s holding with the correct component by where that component sits in the render tree.
在这里,只有一个 <Counter />
JSX 标签,但它被渲染在两个不同的位置:
¥Here, there is only one <Counter />
JSX tag, but it’s rendered at two different positions:
import { useState } from 'react'; export default function App() { const counter = <Counter />; return ( <div> {counter} {counter} </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
这是树的样子:
¥Here’s how these look as a tree:
这是两个独立的计数器,因为每个计数器都在树中自己的位置渲染。你通常不需要考虑这些位置来使用 React,但了解它是如何工作的会很有用。
¥These are two separate counters because each is rendered at its own position in the tree. You don’t usually have to think about these positions to use React, but it can be useful to understand how it works.
在 React 中,屏幕上的每个组件都有完全隔离的状态。例如,如果你并排渲染两个 Counter
组件,它们中的每一个都将获得自己独立的 score
和 hover
状态。
¥In React, each component on the screen has fully isolated state. For example, if you render two Counter
components side by side, each of them will get its own, independent, score
and hover
states.
尝试同时单击两个计数器并注意它们不会相互影响:
¥Try clicking both counters and notice they don’t affect each other:
import { useState } from 'react'; export default function App() { return ( <div> <Counter /> <Counter /> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
如你所见,更新一个计数器时,只会更新该组件的状态:
¥As you can see, when one counter is updated, only the state for that component is updated:
只要你在树中的相同位置渲染相同的组件,React 就会保持状态。要看到这一点,请增加两个计数器,然后通过取消选中 “渲染第二个计数器” 复选框来删除第二个组件,然后通过再次勾选它来将其添加回去:
¥React will keep the state around for as long as you render the same component at the same position in the tree. To see this, increment both counters, then remove the second component by unchecking “Render the second counter” checkbox, and then add it back by ticking it again:
import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return ( <div> <Counter /> {showB && <Counter />} <label> <input type="checkbox" checked={showB} onChange={e => { setShowB(e.target.checked) }} /> Render the second counter </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
注意当你停止渲染第二个计数器时,它的状态是如何完全消失的。那是因为当 React 移除一个组件时,它会破坏它的状态。
¥Notice how the moment you stop rendering the second counter, its state disappears completely. That’s because when React removes a component, it destroys its state.
当你勾选 “渲染第二个计数器” 时,第二个 Counter
及其状态将从头开始 (score = 0
) 初始化并添加到 DOM。
¥When you tick “Render the second counter”, a second Counter
and its state are initialized from scratch (score = 0
) and added to the DOM.
只要组件在 UI 树中的位置渲染,React 就会保留组件的状态。如果它被移除,或者不同的组件在同一位置被渲染,React 会丢弃它的状态。
¥React preserves a component’s state for as long as it’s being rendered at its position in the UI tree. If it gets removed, or a different component gets rendered at the same position, React discards its state.
相同位置的相同组件保留状态
¥Same component at the same position preserves state
在这个例子中,有两个不同的 <Counter />
标签:
¥In this example, there are two different <Counter />
tags:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
当你勾选或清除复选框时,计数器状态不会重置。无论 isFancy
是 true
还是 false
,你总是有一个 <Counter />
作为从根 App
组件返回的 div
的第一个子级:
¥When you tick or clear the checkbox, the counter state does not get reset. Whether isFancy
is true
or false
, you always have a <Counter />
as the first child of the div
returned from the root App
component:
它是相同位置的相同组件,因此从 React 的角度来看,它是相同的计数器。
¥It’s the same component at the same position, so from React’s perspective, it’s the same counter.
同一位置不同组件重置状态
¥Different components at the same position reset state
在此示例中,勾选复选框会将 <Counter>
替换为 <p>
:
¥In this example, ticking the checkbox will replace <Counter>
with a <p>
:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>See you later!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Take a break </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
在这里,你可以在同一位置的不同组件类型之间切换。最初,<div>
的第一个子级包含一个 Counter
。但是当你换入 p
时,React 从 UI 树中删除了 Counter
并销毁了它的状态。
¥Here, you switch between different component types at the same position. Initially, the first child of the <div>
contained a Counter
. But when you swapped in a p
, React removed the Counter
from the UI tree and destroyed its state.
此外,当你在同一位置渲染不同的组件时,它会重置其整个子树的状态。要查看其工作原理,请增加计数器,然后勾选复选框:
¥Also, when you render a different component in the same position, it resets the state of its entire subtree. To see how this works, increment the counter and then tick the checkbox:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
单击复选框时,计数器状态会重置。尽管你渲染 Counter
,但 div
的第一个子级从 div
变为 section
。当子 div
从 DOM 中移除时,它下面的整棵树(包括 Counter
及其状态)也被销毁。
¥The counter state gets reset when you click the checkbox. Although you render a Counter
, the first child of the div
changes from a div
to a section
. When the child div
was removed from the DOM, the whole tree below it (including the Counter
and its state) was destroyed as well.
根据经验,如果你想保留重新渲染之间的状态,则树的结构需要从一个渲染到另一个渲染进行 “匹配”。如果结构不同,状态就会被破坏,因为 React 在从树中移除组件时会破坏状态。
¥As a rule of thumb, if you want to preserve the state between re-renders, the structure of your tree needs to “match up” from one render to another. If the structure is different, the state gets destroyed because React destroys state when it removes a component from the tree.
在同一位置重置状态
¥Resetting state at the same position
默认情况下,React 会在组件处于同一位置时保留其状态。通常,这正是你想要的,因此作为默认行为是有意义的。但有时,你可能想要重置组件的状态。考虑这个应用,它可以让两个玩家在每个回合中跟踪他们的分数:
¥By default, React preserves state of a component while it stays at the same position. Usually, this is exactly what you want, so it makes sense as the default behavior. But sometimes, you may want to reset a component’s state. Consider this app that lets two players keep track of their scores during each turn:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Taylor" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
目前,当你更换玩家时,分数会保留下来。两个 Counter
出现在相同的位置,因此 React 将它们视为同一个 Counter
,其 person
属性已更改。
¥Currently, when you change the player, the score is preserved. The two Counter
s appear in the same position, so React sees them as the same Counter
whose person
prop has changed.
但从概念上讲,在此应用中,它们应该是两个独立的计数器。它们可能出现在 UI 中的相同位置,但一个是 Taylor 的计数器,另一个是 Sarah 的计数器。
¥But conceptually, in this app they should be two separate counters. They might appear in the same place in the UI, but one is a counter for Taylor, and another is a counter for Sarah.
在它们之间切换时有两种重置状态的方法:
¥There are two ways to reset state when switching between them:
-
在不同位置渲染组件
¥Render components in different positions
-
用
key
给每个组件一个明确的标识¥Give each component an explicit identity with
key
选项 1:在不同位置渲染组件
¥Option 1: Rendering a component in different positions
如果想让这两个 Counter
独立,可以在两个不同的位置渲染:
¥If you want these two Counter
s to be independent, you can render them in two different positions:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Taylor" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
-
最初,
isPlayerA
是true
。所以第一个位置包含Counter
状态,第二个是空的。¥Initially,
isPlayerA
istrue
. So the first position containsCounter
state, and the second one is empty. -
当你单击 “下一位玩家” 按钮时,第一个位置将被清除,但第二个位置现在包含
Counter
。¥When you click the “Next player” button the first position clears but the second one now contains a
Counter
.
每个 Counter
的状态每次从 DOM 中删除时都会被销毁。这就是为什么每次单击按钮时它们都会重置。
¥Each Counter
’s state gets destroyed each time it’s removed from the DOM. This is why they reset every time you click the button.
当你只在同一个地方渲染几个独立的组件时,此解决方案很方便。在此示例中,你只有两个,因此在 JSX 中分别渲染两者并不麻烦。
¥This solution is convenient when you only have a few independent components rendered in the same place. In this example, you only have two, so it’s not a hassle to render both separately in the JSX.
选项 2:使用键来重置状态
¥Option 2: Resetting state with a key
还有另一种更通用的方法来重置组件的状态。
¥There is also another, more generic, way to reset a component’s state.
当 渲染列表 你可能已经看到了 key
。键不仅仅用于列表时!你可以使用键让 React 区分任何组件。默认情况下,React 使用父级 (“第一个计数器”, “第二个计数器”) 中的顺序来区分组件。但是键让你告诉 React 这不仅仅是第一个计数器或第二个计数器,而是一个特定的计数器 - 例如,Taylor 的计数器。这样,React 就会知道 Taylor 计数器出现在树中的任何地方!
¥You might have seen key
s when rendering lists. Keys aren’t just for lists! You can use keys to make React distinguish between any components. By default, React uses order within the parent (“first counter”, “second counter”) to discern between components. But keys let you tell React that this is not just a first counter, or a second counter, but a specific counter—for example, Taylor’s counter. This way, React will know Taylor’s counter wherever it appears in the tree!
在这个例子中,两个 <Counter />
不共享状态,即使它们出现在 JSX 中的同一个地方:
¥In this example, the two <Counter />
s don’t share state even though they appear in the same place in JSX:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
在 Taylor 和 Sarah 之间切换不会保留状态。这是因为你给了他们不同的 key
:
¥Switching between Taylor and Sarah does not preserve the state. This is because you gave them different key
s:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
指定 key
告诉 React 使用 key
本身作为位置的一部分,而不是它们在父级中的顺序。这就是为什么,即使你在 JSX 中将它们渲染在同一个位置,React 也会将它们视为两个不同的计数器,因此它们永远不会共享状态。每当一个计数器出现在屏幕上时,它的状态就会被创建。每次删除它时,它的状态都会被破坏。在它们之间切换会一遍又一遍地重置它们的状态。
¥Specifying a key
tells React to use the key
itself as part of the position, instead of their order within the parent. This is why, even though you render them in the same place in JSX, React sees them as two different counters, and so they will never share state. Every time a counter appears on the screen, its state is created. Every time it is removed, its state is destroyed. Toggling between them resets their state over and over.
使用键重置表单
¥Resetting a form with a key
使用键重置状态在处理表单时特别有用。
¥Resetting state with a key is particularly useful when dealing with forms.
在此聊天应用中,<Chat>
组件包含文本输入状态:
¥In this chat app, the <Chat>
component contains the text input state:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
尝试在输入框中输入内容,然后按 “Alice” 或 “Bob” 选择不同的收件人。你会注意到输入状态被保留,因为 <Chat>
在树中的相同位置渲染。
¥Try entering something into the input, and then press “Alice” or “Bob” to choose a different recipient. You will notice that the input state is preserved because the <Chat>
is rendered at the same position in the tree.
在许多应用中,这可能是所需的行为,但在聊天应用中却不是!你不希望让用户由于意外点击而将他们已经输入的消息发送给错误的人。要修复它,请添加 key
:
¥In many apps, this may be the desired behavior, but not in a chat app! You don’t want to let the user send a message they already typed to a wrong person due to an accidental click. To fix it, add a key
:
<Chat key={to.id} contact={to} />
这可确保当你选择不同的收件人时,将从头开始重新创建 Chat
组件,包括其下方树中的任何状态。React 还将重新创建 DOM 元素,而不是重用它们。
¥This ensures that when you select a different recipient, the Chat
component will be recreated from scratch, including any state in the tree below it. React will also re-create the DOM elements instead of reusing them.
现在切换收件人总是会清除文本字段:
¥Now switching the recipient always clears the text field:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.id} contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
深入研究
¥Preserving state for removed components
在真实的聊天应用中,你可能希望在用户再次选择之前的收件人时恢复输入状态。有几种方法可以使不再可见的组件保持状态 “仍然存在”:
¥In a real chat app, you’d probably want to recover the input state when the user selects the previous recipient again. There are a few ways to keep the state “alive” for a component that’s no longer visible:
-
你可以渲染所有聊天而不是当前的聊天,但使用 CSS 隐藏所有其他聊天。聊天记录不会从树中删除,因此它们的本地状态将被保留。此解决方案非常适合简单的 UI。但是如果隐藏树很大并且包含很多 DOM 节点,它会变得非常慢。
¥You could render all chats instead of just the current one, but hide all the others with CSS. The chats would not get removed from the tree, so their local state would be preserved. This solution works great for simple UIs. But it can get very slow if the hidden trees are large and contain a lot of DOM nodes.
-
你可以 提升状态 并为父组件中的每个收件人保留待处理消息。这样,当子组件被删除时,这并不重要,因为保留重要信息的是父级。这是最常见的解决方案。
¥You could lift the state up and hold the pending message for each recipient in the parent component. This way, when the child components get removed, it doesn’t matter, because it’s the parent that keeps the important information. This is the most common solution.
-
除了 React 状态之外,你还可以使用不同的来源。例如,你可能希望即使用户意外关闭页面也能保留消息草稿。要实现这一点,你可以让
Chat
组件通过读取localStorage
来初始化其状态,并将草稿也保存在那里。¥You might also use a different source in addition to React state. For example, you probably want a message draft to persist even if the user accidentally closes the page. To implement this, you could have the
Chat
component initialize its state by reading from thelocalStorage
, and save the drafts there too.
无论你选择哪种策略,与 Alice 的聊天在概念上都不同于与 Bob 的聊天,因此根据当前接收者将 key
赋予 <Chat>
树是有意义的。
¥No matter which strategy you pick, a chat with Alice is conceptually distinct from a chat with Bob, so it makes sense to give a key
to the <Chat>
tree based on the current recipient.
回顾
-
只要同一组件在同一位置渲染,React 就会保持状态。
¥React keeps state for as long as the same component is rendered at the same position.
-
状态不保存在 JSX 标签中。它与你放置该 JSX 的树位置相关联。
¥State is not kept in JSX tags. It’s associated with the tree position in which you put that JSX.
-
你可以通过给它一个不同的键来强制子树重置其状态。
¥You can force a subtree to reset its state by giving it a different key.
-
不要嵌套组件定义,否则你会不小心重置状态。
¥Don’t nest component definitions, or you’ll reset state by accident.
挑战 1 / 5: 修复消失的输入文本
¥Fix disappearing input text
此示例在你按下按钮时显示一条消息。但是,按下按钮也会意外重置输入。为什么会这样?修复它,以便按下按钮不会重置输入文本。
¥This example shows a message when you press the button. However, pressing the button also accidentally resets the input. Why does this happen? Fix it so that pressing the button does not reset the input text.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Hint: Your favorite city?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Hide hint</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Show hint</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }