文章目录
- [用 State 响应输入](#用 State 响应输入)
-
- [声明式地考虑 UI](#声明式地考虑 UI)
-
- [步骤 1:定位组件中不同的视图状态](#步骤 1:定位组件中不同的视图状态)
- [步骤 2:确定是什么触发了这些状态的改变](#步骤 2:确定是什么触发了这些状态的改变)
- [步骤 3:通过 useState 表示内存中的 state](#步骤 3:通过 useState 表示内存中的 state)
- [步骤 4:删除任何不必要的 state 变量](#步骤 4:删除任何不必要的 state 变量)
- [步骤 5:连接事件处理函数以设置 state](#步骤 5:连接事件处理函数以设置 state)
- [选择 State 结构](#选择 State 结构)
-
- [构建 state 的原则](#构建 state 的原则)
- 在组件间共享状态
-
- 举例说明一下状态提升
-
- [第 1 步: 从子组件中移除状态](#第 1 步: 从子组件中移除状态)
- [第 2 步: 从公共父组件传递硬编码数据](#第 2 步: 从公共父组件传递硬编码数据)
- [第 3 步: 为公共父组件添加状态](#第 3 步: 为公共父组件添加状态)
-
- [对 state 进行保留和重置](#对 state 进行保留和重置)
-
- 状态与渲染树中的位置相关
- [相同位置的相同组件会使得 state 被保留下来](#相同位置的相同组件会使得 state 被保留下来)
- [相同位置的不同组件会使 state 重置](#相同位置的不同组件会使 state 重置)
- [在相同位置重置 state](#在相同位置重置 state)
-
- 方法一:将组件渲染在不同的位置
- [方法二:使用 key 来重置 state](#方法二:使用 key 来重置 state)
-
- [为被移除的组件保留 state](#为被移除的组件保留 state)
- [迁移状态逻辑至 Reducer 中](#迁移状态逻辑至 Reducer 中)
-
- [使用 reducer 整合状态逻辑](#使用 reducer 整合状态逻辑)
-
- [第 1 步: 将设置状态的逻辑修改成 dispatch 的一个 action](#第 1 步: 将设置状态的逻辑修改成 dispatch 的一个 action)
- [第 2 步: 编写一个 reducer 函数](#第 2 步: 编写一个 reducer 函数)
- 语法
- 示例
- 使用场景
- 注意事项
- 示例
- [第 3 步: 在组件中使用 reducer](#第 3 步: 在组件中使用 reducer)
- [对比 useState 和 useReducer](#对比 useState 和 useReducer)
- [使用 Context 深层传递参数](#使用 Context 深层传递参数)
-
- [传递 props 带来的问题](#传递 props 带来的问题)
- [Context:传递 props 的另一种方法](#Context:传递 props 的另一种方法)
- [写在你使用 context 之前](#写在你使用 context 之前)
- [Context 的使用场景](#Context 的使用场景)
- [使用 Reducer 和 Context 拓展你的应用](#使用 Reducer 和 Context 拓展你的应用)
- 应急方案
- [使用 ref 引用值](#使用 ref 引用值)
-
- [给你的组件添加 ref](#给你的组件添加 ref)
- [ref 和 state 的不同之处](#ref 和 state 的不同之处)
-
-
- [useRef 内部是如何运行的?](#useRef 内部是如何运行的?)
- [何时使用 ref](#何时使用 ref)
- [ref 的最佳实践](#ref 的最佳实践)
- [使用 ref 操作 DOM](#使用 ref 操作 DOM)
-
- [获取指向节点的 ref](#获取指向节点的 ref)
- [访问另一个组件的 DOM 节点](#访问另一个组件的 DOM 节点)
- [使用 Effect 同步](#使用 Effect 同步)
-
- [如何编写 Effect](#如何编写 Effect)
-
- [第一步:声明 Effect](#第一步:声明 Effect)
- [第二步:指定 Effect 依赖](#第二步:指定 Effect 依赖)
- 第三部:按需添加清理(cleanup)函数
- 订阅事件
- 触发动画
- [初始化应用时不需要使用 Effect 的情形](#初始化应用时不需要使用 Effect 的情形)
- [你可能不需要 Effect](#你可能不需要 Effect)
- [响应式 Effect 的生命周期](#响应式 Effect 的生命周期)
-
- [Effect 的生命周期](#Effect 的生命周期)
\]以下内容来自官方文档 https://zh-hans.react.dev/learn\](https://zh-hans.react.dev/learn)
## 用 State 响应输入
React 控制 UI 的方式是声明式的。你不必直接控制 UI 的各个部分,只需要声明组件可以处于的不同状态,并根据用户的输入在它们之间切换
### 声明式地考虑 UI
你已经从上面的例子看到如何去实现一个表单了,为了更好地理解如何在 React 中思考,接下来你将会学到如何用 React 重新实现这个 UI:
1. **定位**你的组件中不同的视图状态
2. **确定**是什么触发了这些 state 的改变
3. **表示**内存中的 state(需要使用 useState)
4. **删除**任何不必要的 state 变量
5. **连接** 事件处理函数去设置 state
#### 步骤 1:定位组件中不同的视图状态
在计算机科学中,你或许听过可处于多种"状态"之一的 ["状态机"](https://en.wikipedia.org/wiki/Finite-state_machine)
首先,你需要去可视化 UI 界面中用户可能看到的所有不同的"状态":
* **无数据**:表单有一个不可用状态的"提交"按钮。
* **输入中**:表单有一个可用状态的"提交"按钮。
* **提交中**:表单完全处于不可用状态,加载动画出现。
* **成功时**:显示"成功"的消息而非表单。
* **错误时** :与输入状态类似,但会多错误的消息。
#### 步骤 2:确定是什么触发了这些状态的改变
你可以触发 state 的更新来响应两种输入:
* **人为**输入。比如点击按钮、在表单中输入内容,或导航到链接。
* **计算机**输入。比如网络请求得到反馈、定时器被触发,或加载一张图片
你需要改变 state 以响应几个不同的输入:
* **改变输入框中的文本时** (人为)应该根据输入框的内容是否是**空值** ,从而决定将表单的状态从空值状态切换到**输入中**或切换回原状态。
* **点击提交按钮时** (人为)应该将表单的状态切换到**提交中**的状态。
* **网络请求成功后** (计算机)应该将表单的状态切换到**成功**的状态。
* **网络请求失败后** (计算机)应该将表单的状态切换到**失败**的状态,与此同时,显示错误信息
* 
#### 步骤 3:通过 useState 表示内存中的 state
如果你很难立即想出最好的办法,那就先从添加足够多的 state 开始,**确保**所有可能的视图状态都囊括其中:
```js
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
```
你最初的想法或许不是最好的,但是没关系,重构 state 也是步骤中的一部分!
#### 步骤 4:删除任何不必要的 state 变量
在清理之后,你只剩下 3 个(从原本的 7 个!)_必要_的 state 变量:
```js
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
```
#### 步骤 5:连接事件处理函数以设置 state
练习1
```js
import {useState} from 'react'
export default function Picture() {
const [imgcss,setImgcss] = useState(false)
function handleBgd(){
setImgcss(false)
}
function handleImg(e){
e.stopPropagation()
setImgcss(true)
}
let bgdClassName = 'background'
let imgClassName = 'picture'
if(imgcss){
imgClassName += ' picture--active' //注意此处的空格
}else{
bgdClassName += ' background--active' //注意此处的空格
}
return (
setImgcss(false)}>
![]()
{
e.stopPropagation();
setImgcss(true)}}
className={imgClassName}
alt="Rainbow houses in Kampung Pelangi, Indonesia"
src="https://i.imgur.com/5qwVYb1.jpeg"
/>
);
}
```
练习2
```js
import {useState} from 'react'
export default function EditProfile() {
const [edit,setEdit]= useState(false)
const [firstName,setFirstName] = useState('')
const [lastName,setLastName] = useState('')
return (
);
}
```
## 选择 State 结构
### 构建 state 的原则
当你编写一个存有 state 的组件时,你需要选择使用多少个 state 变量以及它们都是怎样的数据格式。尽管选择次优的 state 结构下也可以编写正确的程序,但有几个原则可以指导您做出更好的决策:
1. **合并关联的 state**。如果你总是同时更新两个或更多的 state 变量,请考虑将它们合并为一个单独的 state 变量。
2. **避免互相矛盾的 state**。当 state 结构中存在多个相互矛盾或"不一致"的 state 时,你就可能为此会留下隐患。应尽量避免这种情况。
3. **避免冗余的 state**。如果你能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应将这些信息放入该组件的 state 中。
4. **避免重复的 state**。当同一数据在多个 state 变量之间或在多个嵌套对象中重复时,这会很难保持它们同步。应尽可能减少重复。
5. **避免深度嵌套的 state** 。深度分层的 state 更新起来不是很方便。如果可能的话,最好以扁平化方式构建 state。
## 在组件间共享状态
有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为"状态提升",这是编写 React 代码时常做的事
### 举例说明一下状态提升
在这个例子中,父组件 Accordion 渲染了 2 个独立的 Panel 组件。
* Accordion
* Panel
* Panel
每个 Panel 组件都有一个布尔值 isActive,用于控制其内容是否可见
```js
import { useState } from 'react';
function Panel({ title, children }) {
const [isActive, setIsActive] = useState(false);
return (
{title}
{isActive ? (
{children}
) : (
)}
);
}
export default function Accordion() {
return (
<>
哈萨克斯坦,阿拉木图
阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
这个名字来自于 алма,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,Malus sieversii 被认为是现今苹果的祖先。
>
);
}
```
**假设现在您想改变这种行为,以便在任何时候只展开一个面板** 。在这种设计下,展开第 2 个面板应会折叠第 1 个面板。您该如何做到这一点呢?"
要协调好这两个面板,我们需要分 3 步将状态"提升"到他们的父组件中。
1. 从子组件中 **移除** state 。
2. 从父组件 **传递** 硬编码数据。
3. 为共同的父组件添加 state ,并将其与事件处理函数一起向下传递
#### 第 1 步: 从子组件中移除状态
你将把 Panel 组件对 isActive 的控制权交给他们的父组件。这意味着,父组件会将 isActive 作为 prop 传给子组件 Panel。我们先从 Panel 组件中 **删除下面这一行**:
```jsx
const [isActive, setIsActive] = useState(false);
```
然后,把 isActive 加入 Panel 组件的 props 中:
```js
function Panel({ title, children, isActive }) {
```
#### 第 2 步: 从公共父组件传递硬编码数据
```js
import { useState } from 'react';
export default function Accordion() {
return (
<>
哈萨克斯坦,阿拉木图
阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
这个名字来自于 алма,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,Malus sieversii 被认为是现今苹果的祖先。
>
);
}
function Panel({ title, children, isActive }) {
return (
{title}
{isActive ? (
{children}
) : (
)}
);
}
```
#### 第 3 步: 为公共父组件添加状态
状态提升通常会改变原状态的数据存储类型。
在这个例子中,一次只能激活一个面板。这意味着 Accordion 这个父组件需要记录 **哪个** 面板是被激活的面板。我们可以用数字作为当前被激活 Panel 的索引,而不是 boolean 值:
```js
const [activeIndex, setActiveIndex] = useState(0);
```
```js
import { useState } from 'react';
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
哈萨克斯坦,阿拉木图
setActiveIndex(0)}
>
阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
setActiveIndex(1)}
>
这个名字来自于 алма,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,Malus sieversii 被认为是现今苹果的祖先。
>
);
}
function Panel({
title,
children,
isActive,
onShow
}) {
return (
{title}
{isActive ? (
{children}
) : (
)}
);
}
```

##### 受控组件和非受控组件
通常我们把包含"不受控制"状态的组件称为"非受控组件"。例如,最开始带有 isActive 状态变量的 Panel 组件就是不受控制的,因为其父组件无法控制面板的激活状态。
相反,当组件中的重要信息是由 props 而不是其自身状态驱动时,就可以认为该组件是"受控组件"。这就允许父组件完全指定其行为。最后带有 isActive 属性的 Panel 组件是由 Accordion 组件控制的
练习1
```js
import { useState } from 'react';
export default function SyncedInputs() {
const [text,setText]= useState('')
function handleChange(e){
setText(e.target.value)
}
return (
<>
>
);
}
function Input({ label, onChange, text }) {
//const [text, setText] = useState('');
/*function handleChange(e) {
setText(e.target.value);
}*/
return (
);
}
```
练习2
```js
import { useState } from 'react';
import { foods, filterItems } from './data.js';
export default function FilterableList() {
const [query,setQuery] = useState('')
function handleQuery(e){
setQuery(e.target.value)
}
return (
<>
>
);
}
function SearchBar({text,onChange}) {
/*const [query, setQuery] = useState('');
function handleChange(e) {
setQuery(e.target.value);
}*/
return (
);
}
function List({ items,text }) {
return (
{filterItems(items,text).map(food => (
{food.name} |
{food.description} |
))}
);
}
```
## 对 state 进行保留和重置
### 状态与渲染树中的位置相关
只有当在树中相同的位置渲染相同的组件时,React 才会一直保留着组件的 state
```js
import { useState } from 'react';
export default function App() {
const [showB, setShowB] = useState(true);
return (
{showB && }
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
setHover(true)}
onPointerLeave={() => setHover(false)}
>
{score}
);
}
```
注意,当你停止渲染第二个计数器的那一刻,它的 state 完全消失了。这是因为 React 在移除一个组件时,也会销毁它的 state
当你重新勾选"渲染第二个计数器"复选框时,另一个计数器及其 state 将从头开始初始化(score = 0)并被添加到 DOM 中。

### 相同位置的相同组件会使得 state 被保留下来
```js
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
{isFancy ? (
) : (
)}
);
}
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 (
setHover(true)}
onPointerLeave={() => setHover(false)}
>
{score}
);
}
```

### 相同位置的不同组件会使 state 重置
在这个例子中,勾选复选框会将 替换为一个
```js
import { useState } from 'react';
export default function App() {
const [isPaused, setIsPaused] = useState(false);
return (
{isPaused ? (
待会见!
) : (
)}
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
setHover(true)}
onPointerLeave={() => setHover(false)}
>
{score}
);
}
```
并且,**当你在相同位置渲染不同的组件时,组件的整个子树都会被重置**。要验证这一点,可以增加计数器的值然后勾选复选框
```js
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
{isFancy ? (
) : (
)}
);
}
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 (
setHover(true)}
onPointerLeave={() => setHover(false)}
>
{score}
);
}
```

### 在相同位置重置 state
#### 方法一:将组件渲染在不同的位置
```js
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
{isPlayerA &&
}
{!isPlayerA &&
}
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
setHover(true)}
onPointerLeave={() => setHover(false)}
>
{person} 的分数:{score}
);
}
```

#### 方法二:使用 key 来重置 state
key 不只可以用于列表!你可以使用 key 来让 React 区分任何组件。默认情况下,React 使用父组件内部的顺序("第一个计数器"、"第二个计数器")来区分组件。但是 key 可以让你告诉 React 这不仅仅是 **第一个** 或者 **第二个** 计数器,而且还是一个特定的计数器------例如,**Taylor 的** 计数器。这样无论它出现在树的任何位置, React 都会知道它是 **Taylor 的** 计数器!
```js
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
{isPlayerA ? (
) : (
)}
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
setHover(true)}
onPointerLeave={() => setHover(false)}
>
{person} 的分数:{score}
);
}
```
##### 为被移除的组件保留 state
在真正的聊天应用中,你可能会想在用户再次选择前一个收件人时恢复输入 state。对于一个不可见的组件,有几种方法可以让它的 state "活下去":
* 与其只渲染现在这一个聊天,你可以把 **所有** 聊天都渲染出来,但用 CSS 把其他聊天隐藏起来。这些聊天就不会从树中被移除了,所以它们的内部 state 会被保留下来。这种解决方法对于简单 UI 非常有效。但如果要隐藏的树形结构很大且包含了大量的 DOM 节点,那么性能就会变得很差。
* 你可以进行 [状态提升](https://zh-hans.react.dev/learn/sharing-state-between-components) 并在父组件中保存每个收件人的草稿消息。这样即使子组件被移除了也无所谓,因为保留重要信息的是父组件。这是最常见的解决方法。
* 除了 React 的 state,你也可以使用其他数据源。例如,也许你希望即使用户不小心关闭页面也可以保存一份信息草稿。要实现这一点,你可以让 Chat 组件通过读取 [localStorage](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/localStorage) 对其 state 进行初始化,并把草稿保存在那里。
练习1
```js
import { useState } from 'react';
export default function App() {
const [showHint, setShowHint] = useState(false);
if (showHint) {
return (
提示:你最喜欢的城市?
);
}
return (
);
}
function Form() {
const [text, setText] = useState('');
return (