react 到2023年,仍然蝉联javascript 明星框架项目榜首:
可以看到2023年增量最高的框架,仍然是react!所以学习react有多重要不言而喻。
学习React,当然是看官网,毕竟人家官网文档打磨过那么多次,通篇读下会有许多收获,每个章节学习完还有对应习题练习,确实不错!让我们愉快的学习react吧!
从首页 可以看到(最重要部分是中间),分为两大块 内容: 学习React | API参考
React 哲学
通过一个案例,循序渐进的介绍 开发react 应用的一些思想,
描述UI
第一个组件:
(组件) 应用程序中可复用的UI元素 叫做组件。React 组件是一段可以 使用标签进行扩展 的 JavaScript 函数,这个函数返回JSX标签。
导入和导出组件:
默认导出 与具体名导出;
使用JSX 标签:
JSX 与react 是独立的, JSX 是JavaScript 语法扩展;
随着 Web 的交互性越来越强,逻辑越来越决定页面中的内容。JavaScript 负责 HTML 的内容!这也是为什么 在 React 中,渲染逻辑和标签共同存在于同一个地方------组件。
JSX规则:
-
只返回一个根元素
-
标签必须闭合(原生li不需要闭合)
-
驼峰式命名法
{ js 函数表达式 } JSX中通过大括号使用JavaScript表达式
将 Props 传递给组件
props 可以 传递对象,数组和函数,因为js中 函数也是对象
props 是组件的唯一参数,它的作用与函数参数相同。
props 规则:
- props可以赋值为默认值, 当被设置为时, 当传递 null 和 0, 默认值将不被使用;
- JSX标签中时,下面的 Card 组件将接收一个被设为 的 children prop 并将其包裹在 div 中渲染 ,嵌套的JSX中 , 父组件会存在 子组件内容 到 children 属性里;
- Props是只读的时间快照,每次渲染都会收到新的props,旧的将被js引擎回收;
- 需要交互性时,不能改变props,你可以设置state;
javascript
function Card({children}){
return (
<div>
{ children }
</div>);
}
export default function Profile(){
return (
<Card>
<Avatar/>
</Card>
)
}
条件渲染
与运算符(&&)
可以 使用选择性语句返回JSX表达式;
在JSX里,React 会将false 视为一个"空值",就像null 或者 undefinded,React 不会在这里进行任何渲染。不做任何渲染。切勿将数字放在&& 左侧,JSX更多是一个渲染模板,不关心真实数据是什么。
另外,可以通过定义对象枚举的方式,进行条件判断。
javascript
const drinks = {
tea: {
part: 'leaf',
caffeine: '15--70 mg/cup',
age: '4,000+ years'
},
coffee: {
part: 'bean',
caffeine: '80--185 mg/cup',
age: '1,000+ years'
}
};
function Drink({ name }) {
const info = drinks[name];
return (
<section>
<h1>{name}</h1>
<dl>
<dt>Part of plant</dt>
<dd>{info.part}</dd>
<dt>Caffeine content</dt>
<dd>{info.caffeine}</dd>
<dt>Age</dt>
<dd>{info.age}</dd>
</dl>
</section>
);
}
export default function DrinkList() {
return (
<div>
<Drink name="tea" />
<Drink name="coffee" />
</div>
);
}
渲染列表
React 种支持使用filter筛选和map把数组转换成组件数组,对于组件数组的渲染,需要通过key来唯一标识每一项元素,key 需要满足的条件包括:
如果渲染的列表项显示多个节点的话:可以使用 标签包裹,Fragment 标签本身并不会出现在 DOM 上
javascript
import { Fragment } from 'react';
// ...
const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);
- key 值在兄弟节点之间必须是唯一的, 不要求全局唯一,不同数组可以使用相同key.
- key 值不能改变: 不要在渲染时,动态地生成key。比如key={Math.random()}
通过给集合中的每个组件设置一个 key 值:它使 React 能追踪这些组件,即便后者的位置或数据发生了变化。
保持组件纯粹
什么是纯函数:
- 只负责自己的任务。 只在自己函数作用域范围内操作,不修改范围外任何对象或变量。
- 输入相同,则输出相同。
你不应该改变组件用于渲染的任何输入。这包括 props、state 和 context。通过 "设置" state) 来更新界面,而不要改变预先存在的对象
努力在你返回的 JSX 中表达你的组件逻辑。当你需要"改变事物"时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用 useEffect。
局部 mutation(突变) ; 如果组件改变了预先存在的变量的值,会在渲染过程中出现意想不到的bug;如果必须要改变,必须先对其进行拷贝,保持 mutation 在 函数作用内(局部起作用),使你的组件渲染函数保持纯粹,不要原地修改外部变量数据。
记住数组上的哪些操作会修改原始数组、哪些不会,这非常有帮助。例如,push、pop、reverse 和 sort 会改变原始数组,但 slice、filter 和 map 则会创建一个新数组。
添加交互
响应事件
事件处理函数是绝佳的执行副作用的地方(修改某些值),事件对象传播会经过 三个阶段,先捕获->目标元素->冒泡,
在React中所有事件都会传播,除了 onScroll,它仅适用于你附加到的 JSX 标签
极少数情况下,你可能需要捕获子元素上的所有事件,即便它们阻止了传播。例如,你可能想对每次点击进行埋点记录,
e.stopPropagation 阻止冒泡; e.preventDefault 阻止浏览器默认行为(eg:
提交表单会重刷新页面)
state: 组件的记忆
使用局部变量的问题:
handleClick() 事件处理函数正在更新局部变量 index。但存在两个原因使得变化不可见:
- 局部变量无法在多次渲染中持久保存。 当 React 再次渲染这个组件时,它会从头开始渲染------不会考虑之前对局部变量的任何更改。
- 更改局部变量不会触发渲染。 React 没有意识到它需要使用新数据再次渲染组件。
要使用新数据更新组件,需要做两件事:
- 保留 渲染之间的数据。
- 触发 React 使用新数据渲染组件(重新渲染)。
useState Hook 提供了这两个功能:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
React 如何知道 对应那次渲染,需要返回那个state?
在React内部,为每个组件保存了一个数组, 里面每一项是一个state对, react 通过维护state对的索引值,来确定每次渲染,渲染前设置为0,每次调用useState时,React 会为你提供一个state对,并增加索引值。
Hook 是以 use 开头的特殊函数,在同一组件的每次渲染中,由于Hooks 需要在组件顶部调用,因此Hooks 会有一个稳定的调用顺序。
渲染和提交
用户请求交互界面到渲染网页,包含了三个步骤: 触发 、渲染和提交;
- 触发分为两种情况:
-
- 组件初次渲染
- 组件( 或者其祖先之一 ) 的状态发生改变
类比: 顾客下订单
- 渲染 (渲染是一次纯计算,输入相同,输出必须相同)
- 在进行初次渲染时, React 会调用根组件。
- 对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件。
渲染是递归的,会触发嵌套组件,一层层渲染下去,直到没有更多的嵌套组件并且 React 确切知道哪些组件需要继续渲染。
- 提交
把更改提交到DOM上,
- 对于初次渲染, React 会使用 appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上。
- 对于重渲染, React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。
React 仅在渲染之间存在差异时才会更改 DOM 节点。
如果渲染结果与上次一样,那么 React 将不会修改 DOM (diff算法)
- 浏览器绘制(渲染)
state 如同一张快照⭐⭐⭐⭐⭐
每次渲染 React 会调用你的组件 --- 函数。它返回的JSX 就像是UI的一张及时快照。它的props、事件处理函数和内部变量都是根据当时渲染时的state 计算出来的。
React 重新渲染一个组件时:
- React 会再次调用你组件函数;
- 函数返回新的JSX快照;
- React 会更新界面来匹配你返回快照;
⭐⭐⭐⭐⭐切记 :你的组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值 被计算出来的!
state 不同于函数组件普通变量, 会函数返回后,就会消失;state 是独立于组件外,存在于React本身中------------ 就像摆在架子一样。
设置 state 只会为 下一次 渲染变更 state 的值
javascript
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。在 那次渲染的onClick 内部,number 的值即使在调用 setNumber(number + 5) 之后也还是 0。它的值在 React 通过调用你的组件"获取 UI 的快照"时就被"固定"了。
javascript
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}
把一系列 state 更新加入队列
设置组件state会把一次重新渲染加入队列,但有时希望在下次渲染加入队列之前对state执行多次操作。
每次渲染当前的state都是固定的值。 因此setNumber始终执行(0+1);
javascript
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
React 会等到事件处理函数中的所有代码都运行完毕再出来你的state更新。
只有在你的事件处理函数及其中任何代码执行完成 之后 ,UI 才会更新。这种特性也就是 批处理。
不会触发太多的 重新渲染
再渲染前多次更新同一个state,可以传入一个函数,这告诉React,用state值做某件事,而不仅仅是替换它 ,传入的函数必须是纯函数
ini
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
多次setState 会将state 操作传入队列,批量处理后计算出最后state值,进行下次渲染。
传入值是 替换 state;
传入函数是 用state进行某件事;
更新 state 种的对象:
State可以保存任何类型的js值,包括对象,但是你需要通过创建新对象(根据需要更新对象),来更新对象;
对于多属性对象的复制,可以使用扩展语法..., 但注意它只能展开一层对象,对于多层嵌套对象,你需要调用多次扩展语法... ,
对于嵌套较深的对象,可以使用Immer 库来实现它允许您使用方便但不断变化的语法进行编写,并负责为您生成副本。使用 Immer,您编写的代码看起来像是在"违反规则"并改变对象:(写法简洁)
ini
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
为什么不推荐 直接修改state对象呢?
- 优化方向:
常见的React 优化策略是,如果之前的props 或state 与上一个状态相同,则跳过本次渲染。如果从不更改状态,则检查是否任何更新会变得非常快,preObj === obj。 可以确定它内部是否有变化;
- 新特性:
React新特性是将state做为快照,如果你更新上次的快照的state,去更新,这会让你无法使用新特性。
- 一些特殊场景:
对于一些特殊场景如: 撤销和重做;或者展示历史改变;恢复默认值;这些如果使用突变state的操作,将很难添加类似功能;
- 更简单的实现:
react并不依赖突变,它不需要做任何操作,比如监听对象上的属性;总是将它们封装到Proxies中,或者像许多"反应式"解决方案那样在初始化时做其他工作。这也是为什么React允许您将任何对象放入状态,无论对象有多大,都不会有额外的性能或正确性陷阱。
更新state中的数组
更新state中的数组时,也需要同对应一样,先创建备份,再修改拷贝数据,进行更新。
更新数组内部的对象:
更新数组内部的对象内部对象时,需要格外小心,对象看似在不同数组内部,实际,每个数组只是存在其位置的值,它们指向是同一个对象。
当你更新一个嵌套的 state 时,你需要从想要更新的地方创建拷贝值,一直这样,直到顶层。
在下面的例子中,两个不同的艺术品清单有着相同的初始 state。他们本应该互不影响,但是因为一次 mutation,他们的 state 被意外地共享了,勾选一个清单中的事项会影响另外一个清单:
下面这段代码是问题,所在
ini
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // 问题:直接修改了已有的元素
setMyList(myNextList);
可以使用... 拷贝,你需要修改对象;
ini
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// 创建包含变更的*新*对象
return { ...artwork, seen: nextSeen };
} else {
// 没有变更
return artwork;
}
}));
也可以使用Immer , Immer 总是会根据你对 draft 的修改来从头开始构建下一个 state。这使得你的事件处理程序非常的简洁,同时也不会直接修改 state。
ini
const [yourList, updateYourList] = useImmer(
initialList
);
updateYourList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
状态管理
用State响应输入
React 控制UI的方式是声明式的,你不需要直接控制UI的各个部分(命令式),只需要声明组件可以处于的不同状态,并根据用户的输入在它们之间切换。这与设计师对UI的思考方式相似。不同状态对应快照的页面。
React与传统的非框架编程有什么不同:
命令式编程: 你必须从加载动画到按钮地"命令"每个元素,所以这种告诉计算机如何 去更新 UI 的编程方式被称为命令式编程(对每个dom元素,操作逻辑都写在单独js文件,与html页面隔离开来)。
对于独立系统来说,命令式地控制用户界面的效果也不错,但是当处于更加复杂的系统中时,这会造成管理的困难程度指数级地增长。
React 将html(JSX)与js渲染逻辑写在一个文件.tsx里,这样页面渲染逻辑与页面元素布局,通过状态绑定到一起。你只需要声明你想要显示的内容。React就通过计算state等这些值该如何去更新UI。
声明式地考虑UI的五大步骤:
1.定位你组件中不同的视图的状态;
2.确定是什么触发了这些状态的改变 ;
3.通过 useState 表示内存中的 state;
4.删除不必要state变量;
5.连接事件处理函数以设置state;
对表单来说:
1.定位你组件中不同的视图的状态,
- 无数据:表单有一个不可用状态的"提交"按钮。
- 输入中:表单有一个可用状态的"提交"按钮。
- 提交中:表单完全处于不可用状态,加载动画出现。
- 成功时:显示"成功"的消息而非表单。
- 错误时:与输入状态类似,但会多错误的消息。
2.确定是什么触发了这些状态的改变
可视化,每个状态流程:
- 通过 useState 表示内存中的 state
你需要让"变化的部分"尽可能的少。更复杂的程序会产生更多 bug。
- 删除任何不必要的 state 变量;
删除组合有矛盾有问题;重复的state;
5.连接事件处理函数以设置state
选择State结构
- 如果两个 state 变量总是一起更新,请考虑将它们合并为一个。
- 仔细选择你的 state 变量,以避免创建"极难处理"的 state。
- 用一种减少出错更新的机会的方式来构建你的 state。
- 避免冗余和重复的 state,这样您就不需要保持同步。
- 除非您特别想防止更新,否则不要将 props 放入 state 中。
- 对于选择类型的 UI 模式,请在 state 中保存 ID 或索引而不是对象本身。
- 如果深度嵌套 state 更新很复杂,请尝试将其展开扁平化。
组件间共享状态
状态提升:
对应两个关联的面板,当其中一个面板展示,另一个就折叠; 这种需要将状态提升到父级组件中,
状态提升通常会改变原状态的数据存储类型。比如这里就将布尔值转为为 索引值。 状态提升:
父级组件传递 状态 和 事件处理程序 做为props向下传递:
受控组件与非受控组件
受控(由prop驱动,Antd value和onChange) | 非受控 (由state 驱动)
包含"不受控制"状态的组件称为"非受控组件"。例如,最开始带有 isActive 状态变量的 Panel 组件就是不受控制的,因为其父组件无法控制面板的激活状态。
相反,当组件中的重要信息是由 props 而不是其自身状态驱动时,就可以认为该组件是"受控组件"。这就允许父组件完全指定其行为。最后带有 isActive 属性的 Panel 组件是由 Accordion 组件控制的。
非受控组件通常很简单,因为它们不需要太多配置。但是当你想把它们组合在一起使用时,就不那么灵活了。受控组件具有最大的灵活性,但它们需要父组件使用 props 对其进行配置。
在实践中,"受控"和"非受控"并不是严格的技术术语------通常每个组件都同时拥有内部状态和 props。然而,这对于组件该如何设计和提供什么样功能的讨论是有帮助的。
当编写一个组件时,你应该考虑哪些信息应该受控制(通过 props),哪些信息不应该受控制(通过 state)。
对state 进行保留和重置⭐⭐⭐⭐
react 根据你的JSX 生成 UI 树, 之后 React DOM 根据UI树去更新浏览器的DOM元素
- 组件中state与树中的位置有关;
渲染不同位置的两个相同组件,它们的state是相互独立的。
- 相同位置的相同组件会使得state被保留下来。
这里有两个不同Counter组件标签,当勾选时,react依然一直在渲染相同位置的相同组件,所以state被保留下来。
对React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置。
- 不同位置的不同组件会使state重置
组件在UI树中被移除,react也将消耗它的状态
对应UI树的变化:
- 相同的位置重置state
默认情况下,相同位置的相同的组件会保留state,但在一些特殊有应用场景,可能需要我们重置state,如下图所示:需要重置分数
可以使用两种方案解决:
- 将组件渲染到不同位置;
- 使用key标识组件,来重置state;
指定一个 key 能够让 React 将 key 本身而非它们在父组件中的顺序作为位置的一部分。这就是为什么尽管你用 JSX 将组件渲染在相同位置,但在 React 看来它们是两个不同的计数器。因此它们永远都不会共享 state。
使用Reducer整合组件状态逻辑
这块主要介绍了,reducer的使用和一些注意地方;
编写 reducers 时最好牢记以下两点:
- reducers 必须是纯粹的。 这一点和 状态更新函数 是相似的,reducers 在是在渲染时运行的!(actions 会排队直到下一次渲染)。 这就意味着 reducers 必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新 对象 和 数组。
- 每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。 举个例子,如果用户在一个由 reducer 管理的表单(包含五个表单项)中点击了 重置按钮,那么 dispatch 一个 reset_form 的 action 比 dispatch 五个单独的 set_field 的 action 更加合理。如果你在一个 reducer 中打印了所有的 action 日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。这对代码调试很有帮助!
使用Context 深层传递参数
这块主要介绍了Context 的使用
摘要:
- Context 使组件向其下方的整个树提供信息。
- 传递 Context 的方法:
-
- 通过 export const MyContext = createContext(defaultValue) 创建并导出 context。
- 在无论层级多深的任何子组件中,把 context 传递给 useContext(MyContext) Hook 来读取它。
- 在父组件中把 children 包在 <MyContext.Provider value={...}> 中来提供 context。
- Context 会穿过中间的任何组件。
- Context 可以让你写出 "较为通用" 的组件。
- 在使用 context 之前,先试试传递 props 或者将 JSX 作为 children 传递。
使用场景:
- 主题: 如果你的应用允许用户更改其外观(例如暗夜模式),你可以在应用顶层放一个 context provider,并在需要调整其外观的组件中使用该 context。
- 当前账户: 许多组件可能需要知道当前登录的用户信息。将它放到 context 中可以方便地在树中的任何位置读取它。某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。
- 路由: 大多数路由解决方案在其内部使用 context 来保存当前路由。这就是每个链接"知道"它是否处于活动状态的方式。如果你创建自己的路由库,你可能也会这么做。
- 状态管理: 随着你的应用的增长,最终在靠近应用顶部的位置可能会有很多 state。许多遥远的下层组件可能想要修改它们。通常 将 reducer 与 context 搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。
Context 不局限于静态值。如果你在下一次渲染时传递不同的值,React 将会更新读取它的所有下层组件!这就是 context 经常和 state 结合使用的原因。
一般而言,如果树中不同部分的远距离组件需要某些信息,context 将会对你大有帮助。
使用Reducer 和 Context 来扩展应用
- 你可以将 reducer 与 context 相结合,让任何组件读取和更新它的状态。
- 为子组件提供 state 和 dispatch 函数:
-
- 创建两个 context (一个用于 state,一个用于 dispatch 函数)。
- 让组件的 context 使用 reducer。
- 使用组件中需要读取的 context。
- 你可以通过将所有传递信息的代码移动到单个文件中来进一步整理组件。
-
- 你可以导出一个像 TasksProvider 可以提供 context 的组件。
- 你也可以导出像 useTasks 和 useTasksDispatch 这样的自定义 Hook。
- 你可以在你的应用程序中大量使用 context 和 reducer 的组合。
应急方案
使用ref引用值
介绍ref使用场景与注意事项,最后一个例子也很特别;
使用 ref 操作 DOM
使用Effect 同步⭐⭐⭐⭐⭐
Effect定位:
有些组件需要与外部系统同步。 例如,您可能希望根据 React 状态控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。 Effects 允许您在渲染后运行一些代码,以便您可以将组件与 React 之外的某些系统同步。
react 组件中有两种逻辑:
(Rendering code )渲染代码:获取props和state,转换它们,然后返回你想在屏幕上看到JSX. Rendering code 必须是纯粹的,像数学公式一样。它应该只计算结果。不做其他事情。
(Event handlers)事件处理:组件中的嵌套函数,用于执行操作,事件处理程序可能会更新字段,提交http请求,导航页面。包含特定操作(按钮单击或键入)引起的副作用(更改程序状态)Effect就是提供额外的事件处理方案。 (定位)
Effects与events内置的事件处理有和不同?:
Effects let you specify side effects that are caused by rendering itself, rather than by a particular event.( Effect 允许你指定由渲染本身造成,而不是特定的事件引起的副作用。)比如(ChatRoom组件连接,聊天服务器)
执行时机不同, Effect是在渲染结束后去执行,Effect里的代码,而事件回调一般由特定用户动作引起。
不要急于将效果添加到组件中。请记住,效果通常用于"跳出"您的 React 代码并与某些外部系统同步。这包括浏览器 API、第三方小部件、网络等。如果效果仅根据其他状态调整某些状态,则可能不需要效果。
如何使用Effect (跳过)
如何处理Effect在开发模式下调用两次
在开发严格模式下,Effect代码会被调用两次。初次挂载组件后,又会再次触发一次。(React是有意这么做,以查找错误。用户不应区分:运行一次的 Effect(如在生产中)和 设置→清理→设置(如您在开发中看到的那样)
某些开发场景下,你需要清理你副作用:
- 弹框方法调用 (关闭);
- 订阅服务(取消);
- 动画处理(重置);
- 获取异步数据,(中止获取或忽略结果);
某些 API 可能不允许连续调用它们两次。例如,内置元素的 showModal 方法在
调用两次时会引发。实现清理功能并使其关闭对话框:
ini
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
在开发中,你的效果将调用 showModal(),然后立即关闭(),然后再次调用 showModal()。这与调用 showModal() 一次具有相同的用户可见行为,就像您在生产中看到的那样。
Effect 获取了某些内容,则清理函数应该中止获取或忽略其结果:
ini
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
您无法"撤消"已经发生的网络请求,但清理功能应确保不再相关的提取不会继续影响您的应用程序。如果 userId 从"Alice"更改为"Bob",则清理可确保忽略"Alice"响应,即使它在"Bob"之后到达。(解决竞态问题)
例举其他情况例子,对Effect的错误使用:
-
props变化重置所有state;
-
props变化调整部分state;
-
事件处理函数中共享逻辑;
-
发送post请求;
-
使用Effect链式计算;
-
订阅外部store;
-
获取数据;
- (在Effect更新state时):根据props 或者state来调整state时,都会使得树精流更难理解和调试。可以通过添加key来重置state,或者在渲染期间计算所需内容。
- (在Effect实现功能逻辑时):需要区分是放入Effect还是事件处理函数,先自问 为什么 要执行这些代码。Effect 只用来执行那些显示给用户时组件 需要执行 的代码。
- (在Effect中订阅外部store时): 可以使用react专门提供的useSyncExternalStore
- (在Effect中获取数据时):当获取数据的行为不是键入事件时,在Effect中获取数据是可以的,但是需要注意,请求快慢返回不同,导致与你预期返回顺序不符合问题。
为了修复这个问题,你需要添加一个清理函数来忽略较早返回结果:
scss
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
这确保了当你在 Effect 中获取数据时,除了最后一次请求的所有返回结果都将被忽略。
处理竞态条件并不是实现数据获取的唯一难点。你可能还需要考虑缓存响应结果(使用户点击后退按钮时可以立即看到先前的屏幕内容),如何在服务端获取数据(使服务端初始渲染的 HTML 中包含获取到的内容而不是加载动画),以及如何避免网络瀑布(使子组件不必等待每个父组件的数据获取完毕后才开始获取数据)。
这些问题适用于任何 UI 库,而不仅仅是 React。解决这些问题并不容易,这也是为什么Next.js 提供了比在 Effect 中获取数据更有效的内置数据获取机制的原因。
当你不得不写Effect时,需要考虑是否可以将某段功能提取到专门的内置的事件回调api 或一个更声明性的自定义Hook中,在组件中原始的useEffect调用越少,维护应用将变得更加容易。
Effects 中的生命周期
为什么同步可能发生多次;
React 是如果再进行同步的;
从Effect角度去思考;
React 是怎么验证你需要再做一次同步的;
React 是怎么知道你需要再做一次同步的;
每一个Effect代表了独立的同步过程块;
将事件从Effect中分开⭐⭐⭐⭐
响应式与非响应式事件逻辑
这里有一个场景:聊天室组件,需求如下:
组件应该自动连接选中的聊天室。
每当你点击"Send"按钮,组件应该在当前聊天界面发送一条消息。
从直观上来看,事件处理函数总是"手动"触发的,例如点击按钮。另一方面,Effect是自动触发:每当需要保持同步的时候他们就会开始运行和重新执行,如聊天室组件初始挂载的时候,需要去请求连接。
组件内部声明的state和props变量被称为 响应值, 这种响应值可以因为重新渲染而变化;例如用户可能会编辑 message 或者在下拉菜单中选中不同的 roomId。事件处理函数和 Effect 对于变化的响应是不一样的:
事件处理函数内部的逻辑是非响应式的。除非用户又执行了同样的操作(例如点击),否则这段逻辑不会再运行。事件处理函数可以在"不响应"他们变化的情况下读取响应式值。
Effect 内部的逻辑是响应式的 。如果 Effect 要读取响应式值,你必须将它指定为依赖项。如果接下来的重新渲染引起那个值变化,React 就会使用新值重新运行 Effect 内的逻辑。
message 值变化并不意味着用户想发送消息;
scss
function handleSendClick() {
sendMessage(message);
}
rootId值的变化确切的意味着用户想连接不同聊天室。
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);
响应式与非响应式逻辑混用
但是有时候,响应式逻辑和非响应式逻辑可能需要混用。
例如,假设你想在用户连接到聊天室时展示一个通知。并且通过从 props 中读取当前 theme(dark 或者 light)来展示对应颜色的通知:
theme是一个响应式的值,它会由于重新渲染而变化),并且 Effect 读取的每一个响应式值都必须在其依赖项中声明。这会导致主题发生改变,也会触发连接操作;
scss
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
// 希望useEffect中theme始终是最新的值,但是不能触发响应;
// 非响应事件
showNotification('Connected!', theme);
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme])
声明一个Effect Event事件(本章节描述了一个在 React 稳定版中 还没有发布的实验性 API。)
使用useEffectEvent hook 从 Effect中抽离出非响应逻辑
这里onConnected 被称为Effect Event。它是Event逻辑,但更像事件处理函数,内部逻辑不是响应式的,并且可以获取到最新的state,props。
scss
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 声明所有依赖项
这种方式,从useEffect依赖项中移除掉了onConnected,因为它是非响应式的。
你可以将 Effect Event 看成和事件处理函数相似的东西。主要区别是事件处理函数只在响应用户交互的时候运行,而 Effect Event 是你在 Effect 中触发的。Effect Event 让你在 Effect 响应性和不应是响应式的代码间"打破链条"。
Effect Event的局限性
只在 Effect 内部调用他们。
永远不要把他们传给其他的组件或者 Hook。
移除Effect依赖⭐⭐⭐⭐⭐ (具体例子)
依赖应始终与代码匹配。
当你对依赖不满意时,你需要编辑的是代码。
抑制 linter 会导致非常混乱的错误,你应该始终避免它。
要移除依赖,你需要向 linter "证明"它不是必需的。
如果某些代码是为了响应特定交互,请将该代码移至事件处理的地方。
如果 Effect 的不同部分因不同原因需要重新运行,请将其拆分为多个 Effect。
如果你想根据以前的状态更新一些状态,传递一个更新函数。
如果你想读取最新值而不"反应"它,请从 Effect 中提取出一个 Effect Event。
在 JavaScript 中,如果对象和函数是在不同时间创建的,则它们被认为是不同的。
尽量避免对象和函数依赖。将它们移到组件外或 Effect 内。