组件之间的状态是隔离的。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
,你总是在根App
组件返回的div
中拥有一个<Counter />
作为第一个子元素
它是同一个组件,位于相同的位置,因此从 React 的角度来看,它就是同一个计数器。
相同位置的不同组件会重置状态
在这个例子中,勾选复选框会用<p>
替换<Counter>
。
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中渲染它们并不麻烦。
方案二:使用key重置状态
还有一种更通用的方法可以重置组件的状态。
您可能在渲染列表时看到过key
。key不仅仅用于列表!您可以使用key让React区分任何组件。默认情况下,React使用父元素中的顺序(“第一个计数器”、“第二个计数器”)来区分组件。但是,key可以让您告诉React,这不是仅仅是第一个计数器或第二个计数器,而是一个特定的计数器——例如,Taylor的计数器。这样,React就能在树中的任何位置识别Taylor的计数器!
在这个例子中,两个<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 的树状结构位置相关联。
- 可以通过赋予子树不同的键值来强制其重置状态。
- 不要嵌套组件定义,否则会意外地重置状态。
挑战 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)} /> ); }