对 state 进行保留和重置
各个组件的 state 是各自独立的。根据组件在 UI 树中的位置,React 可以跟踪哪些 state 属于哪个组件。你可以控制在重新渲染过程中何时对 state 进行保留和重置。
开发环境:React+ts+antd
学习内容
- React 何时选择保留或重置状态
- 如何强制 React 重置组件的状态
- 键和类型如何影响状态是否被保留
状态与渲染树中的位置相关
React 会为 UI 中的组件结构构建 渲染树。
当向一个组件添加状态时,那么可能会认为状态“存在”在组件内。但实际上,状态是由 React 保存的。React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。
在 React 中,屏幕中的每个组件都有完全独立的 state。举个例子,当你并排渲染两个 Counter 组件时,它们都会拥有各自独立的 score 和 hover state。
试试点击两个 counter 你会发现它们互不影响:
import React, {useState} from 'react';
import {Button, Card, Col, Row} from 'antd';
const App: React.FC = () => {
return (
<Row gutter={16}>
<Counter/>
<Counter/>
</Row>
);
}
export default App
const Counter = () => {
const [score, setScore] = useState(0);
return (
<Col span={6}>
<Card>
<h1>{score}</h1>
<Button type="primary" onClick={() => setScore(score + 1)}>
加一
</Button>
</Card>
</Col>
);
}
如你所见,当一个计数器被更新时,只有该组件的状态会被更新:
只有当在树中相同的位置渲染相同的组件时,React 才会一直保留着组件的 state。想要验证这一点,可以将两个计数器的值递增,取消勾选 “渲染第二个计数器” 复选框,然后再次勾选它:
import React, {useState} from 'react';
import {Button, Card, Col, Row} from 'antd';
const App: React.FC = () => {
const [showB, setShowB] = useState(true);
return (
<>
<Row gutter={16}>
<Counter/>
{showB && <Counter/>}
</Row>
<div style={{marginTop:30}}>
<label>
<input
type="checkbox"
checked={showB}
onChange={e => {
setShowB(e.target.checked)
}}
/>
渲染第二个计数器
</label>
</div>
</>
);
}
export default App
const Counter = () => {
const [score, setScore] = useState(0);
return (
<Col span={6}>
<Card>
<h1>{score}</h1>
<Button type="primary" onClick={() => setScore(score + 1)}>
加一
</Button>
</Card>
</Col>
);
}
再次勾选
注意,当你停止渲染第二个计数器的那一刻,它的 state 完全消失了。这是因为 React 在移除一个组件时,也会销毁它的 state。
当你重新勾选“渲染第二个计数器”复选框时,另一个计数器及其 state 将从头开始初始化(score = 0)并被添加到 DOM 中。
只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。
相同位置的相同组件会使得 state 被保留下来
在这个例子中,有两个不同的 标签:
import React, {useState} from 'react';
import {Button, Card, Col, Row} from 'antd';
const App: React.FC = () => {
const [isFancy, setIsFancy] = useState(false);
return (
<>
<Row gutter={16}>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
</Row>
<div style={{marginTop:30}}>
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
使用好看的样式
</label>
</div>
</>
);
}
export default App
interface CounterProp{
isFancy:boolean
}
const Counter:React.FC<CounterProp> = ({ isFancy }) => {
const [score, setScore] = useState(0);
let className='counter';
if (isFancy) {
className += ' fancyClass';
}
return (
<Col span={6}>
<Card className={className}>
<h1>{score}</h1>
<Button type="primary" onClick={() => setScore(score + 1)}>
加一
</Button>
</Card>
</Col>
);
}
当你勾选或清空复选框的时候,计数器 state 并没有被重置。不管 isFancy 是 true 还是 false,根组件 App 返回的 div 的第一个子组件都是Counter:
它是位于相同位置的相同组件,所以对 React 来说,它是同一个计数器。
相同位置的不同组件会使 state 重置
在这个例子中,勾选复选框会将 Counter 替换为 p 标签:
import React, {useState} from 'react';
import {Button, Card, Col, Row} from 'antd';
const App: React.FC = () => {
const [isFancy, setIsFancy] = useState(false);
return (
<>
<Row gutter={16}>
{isFancy ? (
<p>待会见!</p>
) : (
<Counter />
)}
</Row>
<div style={{marginTop:30}}>
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
休息一下
</label>
</div>
</>
);
}
export default App
const Counter= () => {
const [score, setScore] = useState(0);
return (
<Col span={6}>
<Card>
<h1>{score}</h1>
<Button type="primary" onClick={() => setScore(score + 1)}>
加一
</Button>
</Card>
</Col>
);
}
示例中,你在相同位置对 不同 的组件类型进行切换。刚开始
并且,当你在相同位置渲染不同的组件时,组件的整个子树都会被重置。要验证这一点,可以增加计数器的值然后勾选复选框:
import React, {useState} from 'react';
import {Button, Card, Col, Row} from 'antd';
const App: React.FC = () => {
const [isFancy, setIsFancy] = useState(false);
return (
<>
<Row gutter={16}>
<Col span={6}>
{isFancy ? (
<div>
<Counter isFancy={true}/>
</div>
) : (
<section>
<Counter isFancy={false}/>
</section>
)}
</Col>
</Row>
<div style={{marginTop: 30}}>
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
使用好看的样式
</label>
</div>
</>
);
}
export default App
interface CounterProp {
isFancy: boolean
}
const Counter: React.FC<CounterProp> = ({isFancy}) => {
const [score, setScore] = useState(0);
let className = 'counter';
if (isFancy) {
className += ' fancyClass';
}
return (
<Card className={className}>
<h1>{score}</h1>
<Button type="primary" onClick={() => setScore(score + 1)}>
加一
</Button>
</Card>
);
}
当你勾选复选框后计数器的 state 被重置了。虽然你渲染了一个 Counter,但是 div 的第一个子组件从 div 变成了 section。当子组件 div 从 DOM 中被移除的时候,它底下的整棵树(包含 Counter 以及它的 state)也都被销毁了。
一般来说,如果你想在重新渲染时保留 state,几次渲染中的树形结构就应该相互“匹配”。结构不同就会导致 state 的销毁,因为 React 会在将一个组件从树中移除时销毁它的 state。
在相同位置重置 state
默认情况下,React 会在一个组件保持在同一位置时保留它的 state。通常这就是你想要的,所以把它作为默认特性很合理。但有时候,你可能想要重置一个组件的 state。考虑一下这个应用,它可以让两个玩家在每个回合中记录他们的得分:
import React, {useState} from 'react';
import {Button, Card} from 'antd';
const App: React.FC = () => {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<>
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
<Button color="red" variant="solid" onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
下一位玩家!
</Button>
</>
);
}
export default App
interface CounterProp {
person: string
}
const Counter: React.FC<CounterProp> = ({person}) => {
const [score, setScore] = useState(0);
return (
<Card style={{width:400}}>
<h1>{person} 的得分: {score}</h1>
<Button color="blue" variant="solid" onClick={() => setScore(score + 1)}>
加一
</Button>
</Card>
);
}
目前当你切换玩家时,分数会被保留下来。这两个 Counter 出现在相同的位置,所以 React 会认为它们是 同一个 Counter,只是传了不同的 person prop。
但是从概念上讲,这个应用中的两个计数器应该是各自独立的。虽然它们在 UI 中的位置相同,但是一个是 Taylor 的计数器,一个是 Sarah 的计数器。
有两个方法可以在它们相互切换时重置 state:
- 将组件渲染在不同的位置
- 使用 key 赋予每个组件一个明确的身份
方法一:将组件渲染在不同的位置
你如果想让两个 Counter 各自独立的话,可以将它们渲染在不同的位置:
return (
<>
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
<Button color="red" variant="solid" onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
下一位玩家!
</Button>
</>
);
- 起初 isPlayerA 的值是 true。所以第一个位置包含了 Counter 的 state,而第二个位置是空的。
- 当你点击“下一位玩家”按钮时,第一个位置会被清空,而第二个位置现在包含了一个 Counter。
每当 Counter 组件从 DOM 中移除时,它的 state 会被销毁。这就是每次点击按钮它们就会被重置的原因。
这个解决方案在你只有少数几个独立的组件渲染在相同的位置时会很方便。这个例子中只有 2 个组件,所以在 JSX 里将它们分开进行渲染并不麻烦。
方法二:使用 key 来重置 state
另一种更通用的重置组件 state 的方法。
你可能在 渲染列表 时见到过 key。但 key 不只可以用于列表!你可以使用 key 来让 React 区分任何组件。默认情况下,React 使用父组件内部的顺序(“第一个计数器”、“第二个计数器”)来区分组件。但是 key 可以让你告诉 React 这不仅仅是 第一个 或者 第二个 计数器,而且还是一个特定的计数器——例如,Taylor 的 计数器。这样无论它出现在树的任何位置, React 都会知道它是 Taylor 的 计数器!
在这个例子中,即使两个 会出现在 JSX 中的同一个位置,它们也不会共享 state:
return (
<>
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
<Button color="red" variant="solid" onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
下一位玩家!
</Button>
</>
);
在 Taylor 和 Sarah 之间切换不会使 state 被保留下来。因为 你给他们赋了不同的 key。
指定一个 key 能够让 React 将 key 本身而非它们在父组件中的顺序作为位置的一部分。这就是为什么尽管你用 JSX 将组件渲染在相同位置,但在 React 看来它们是两个不同的计数器。因此它们永远都不会共享 state。每当一个计数器出现在屏幕上时,它的 state 会被创建出来。每当它被移除时,它的 state 就会被销毁。在它们之间切换会一次又一次地使它们的 state 重置。
请记住 key 不是全局唯一的。它们只能指定 父组件内部 的顺序。
摘要
- 只要在相同位置渲染的是相同组件, React 就会保留状态。
- state 不会被保存在 JSX 标签里。它与你在树中放置该 JSX 的位置相关联。
- 你可以通过为一个子树指定一个不同的 key 来重置它的 state。
- 不要嵌套组件的定义,否则你会意外地导致 state 被重置。
说明:文中例子参考React官方文档教程,不同的是这里使用TypeScript 来写,更详细的学习请移步React 官方文档教程