组件之间的状态是隔离的。React 会根据组件在 UI 树中的位置跟踪哪个状态属于哪个组件。您可以控制在重新渲染之间何时保留状态以及何时重置状态。
您将学习
- React 何时选择保留或重置状态
- 如何强制 React 重置组件的状态
- 键和类型如何影响状态是否被保留
状态与渲染树中的位置相关联
React 为您 UI 中的组件结构构建渲染树。
当您为组件提供状态时,您可能会认为状态“存在于”组件内部。但状态实际上是保存在 React 内部。React 通过组件在渲染树中的位置将它持有的每个状态片段与正确的组件相关联。
在这里,只有一个 <Counter />
JSX 标签,但它在两个不同的位置渲染
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> ); }
以下是它们作为树的外观
这是两个独立的计数器,因为每个计数器都在树中的自身位置渲染。 您通常不必考虑这些位置就可以使用 React,但了解它的工作原理可能很有用。
在 React 中,屏幕上的每个组件都有完全隔离的状态。例如,如果您并排渲染两个 Counter
组件,则每个组件都将获得其自身独立的 score
和 hover
状态。
尝试单击两个计数器,并注意它们不会互相影响
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> ); }
如您所见,当一个计数器更新时,只有该组件的状态会更新
只要您在树中的相同位置渲染相同的组件,React 就会一直保留该状态。要查看这一点,请增加两个计数器的值,然后通过取消选中“渲染第二个计数器”复选框删除第二个组件,然后通过再次选中它来添加它
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 删除一个组件时,它会销毁其状态。
当您勾选“渲染第二个计数器”时,会从头开始初始化第二个 Counter
及其状态(score = 0
),并将其添加到 DOM 中。
只要组件在其 UI 树中的位置被渲染,React 就会保留该组件的状态。 如果它被移除,或者在相同位置渲染了不同的组件,React 会丢弃其状态。
相同位置的相同组件会保留状态
在此示例中,有两个不同的 <Counter />
标签
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
的第一个子级
它是相同位置的相同组件,因此从 React 的角度来看,它是同一个计数器。
相同位置的不同组件会重置状态
在此示例中,勾选复选框会将 <Counter>
替换为 <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
并销毁其状态。
此外,当你在相同位置渲染不同的组件时,它会重置其整个子树的状态。 要查看其工作原理,请增加计数器,然后勾选复选框。
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
及其状态)也被销毁了。
根据经验,如果你想在重新渲染之间保留状态,则树的结构需要从一次渲染到下一次渲染“匹配”。 如果结构不同,则状态将被销毁,因为 React 在从树中移除组件时会销毁状态。
在相同位置重置状态
默认情况下,React 会在组件保持在相同位置时保留其状态。通常,这正是你想要的,因此将其作为默认行为是有意义的。但有时,你可能希望重置组件的状态。请考虑这个应用程序,它可以让两个玩家在每个回合中跟踪他们的分数。
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
属性已更改。
但从概念上讲,在这个应用程序中,它们应该是两个独立的计数器。它们可能出现在 UI 中的同一个位置,但一个是 Taylor 的计数器,另一个是 Sarah 的计数器。
有两种方法可以在它们之间切换时重置状态:
- 在不同位置渲染组件。
- 使用
key
为每个组件赋予明确的身份。
选项 1:在不同位置渲染组件
如果你希望这两个 Counter
是独立的,你可以将它们渲染在两个不同的位置。
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
状态,第二个位置为空。 - 当你单击“下一个玩家”按钮时,第一个位置将清除,但第二个位置现在包含一个
Counter
。
每个 Counter
的状态在每次从 DOM 中移除时都会被销毁。这就是为什么每次单击按钮时它们都会重置。
当你只有几个独立的组件渲染在同一个位置时,此解决方案很方便。在此示例中,你只有两个,因此在 JSX 中分别渲染它们并不麻烦。
选项 2:使用 Key 重置状态
还有一种更通用的方法来重置组件的状态。
您可能在渲染列表时见过key
。Key 不仅仅用于列表!您可以使用 key 来使 React 区分任何组件。默认情况下,React 使用父组件中的顺序(“第一个计数器”、“第二个计数器”)来区分组件。但是,key 允许您告诉 React,这不仅仅是*第一个*计数器或*第二个*计数器,而是一个特定的计数器——例如,*Taylor 的*计数器。这样,无论*Taylor 的*计数器出现在树中的哪个位置,React 都能识别它!
在此示例中,即使两个 <Counter />
出现在 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
:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
指定 key
会告诉 React 使用 key
本身作为位置的一部分,而不是它们在父组件中的顺序。这就是为什么,即使您在 JSX 中的相同位置渲染它们,React 也会将它们视为两个不同的计数器,因此它们永远不会共享状态。每次计数器出现在屏幕上时,都会创建其状态。每次将其删除时,其状态都会被销毁。在它们之间切换会反复重置它们的状态。
使用 Key 重置表单
在处理表单时,使用 key 重置状态特别有用。
在这个聊天应用程序中,<Chat>
组件包含文本输入状态
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: '[email protected]' }, { id: 1, name: 'Alice', email: '[email protected]' }, { id: 2, name: 'Bob', email: '[email protected]' } ];
尝试在输入框中输入一些内容,然后按“Alice”或“Bob”选择不同的收件人。您会注意到输入状态被保留,因为 <Chat>
渲染在树中的相同位置。
在许多应用程序中,这可能是预期的行为,但在聊天应用程序中则不然! 您不希望因为用户意外点击而让他们将已经输入的信息发送给错误的人。要解决此问题,请添加一个 key
<Chat key={to.id} contact={to} />
这可确保在您选择其他收件人时,将从头开始重新创建 Chat
组件,包括其下方树中的任何状态。React 还会重新创建 DOM 元素,而不是重复使用它们。
现在切换收件人总是会清除文本字段
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: '[email protected]' }, { id: 1, name: 'Alice', email: '[email protected]' }, { id: 2, name: 'Bob', email: '[email protected]' } ];
深入探讨
在真实的聊天应用程序中,当用户再次选择上一个收件人时,您可能希望恢复输入状态。有一些方法可以为不再可见的组件保留状态的“活动”
- 您可以渲染*所有*聊天,而不仅仅是当前聊天,但使用 CSS 隐藏所有其他聊天。聊天不会从树中删除,因此它们的本地状态将被保留。此解决方案非常适用于简单的 UI。但是,如果隐藏的树很大并且包含许多 DOM 节点,它可能会变得非常慢。
- 您可以将状态提升并在父组件中为每个收件人保留待处理消息。这样,当子组件被删除时,它并不重要,因为保留重要信息的是父组件。这是最常见的解决方案。
- 除了 React 状态之外,您还可以使用其他来源。例如,您可能希望即使用户意外关闭页面也能保留消息草稿。要实现这一点,您可以让
Chat
组件通过从localStorage
读取来初始化其状态,并在那里保存草稿。
无论您选择哪种策略,*与 Alice 的*聊天在概念上都不同于*与 Bob 的*聊天,因此根据当前收件人为 <Chat>
树提供 key
是有意义的。
总结
- 只要在相同位置渲染相同的组件,React 就会保留其状态。
- 状态不保存在 JSX 标签中。它与您放置该 JSX 的树位置相关联。
- 您可以通过为子树提供不同的 key 来强制其重置状态。
- 不要嵌套组件定义,否则您会意外重置状态。
挑战 1的 5: 修复消失的输入文本
此示例在您按下按钮时显示一条消息。但是,按下按钮也会意外重置输入。为什么会发生这种情况?修复它,以便按下按钮不会重置输入文本。
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)} /> ); }