状态与渲染树中的位置相关
React 会为 UI 中的组件结构构建 渲染树。
当向一个组件添加状态时,那么可能会认为状态“存在”在组件内。但实际上,状态是由 React 保存的。React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。
(react中hooks 只能在组件顶层使用,且不能写在循环或者判断里;react执行hook是按顺序执行的。)
写在你使用 context 之前
使用 Context 看起来非常诱人!然而,这也意味着它也太容易被过度使用了。如果你只想把一些 props 传递到多个层级中,这并不意味着你需要把这些信息放到 context 里。
在使用 context 之前,你可以考虑以下几种替代方案:
- 从 传递 props 开始。 如果你的组件看起来不起眼,那么通过十几个组件向下传递一堆 props 并不罕见。这有点像是在埋头苦干,但是这样做可以让哪些组件用了哪些数据变得十分清晰!维护你代码的人会很高兴你用 props 让数据流变得更加清晰。
- 抽象组件并 将 JSX 作为
children传递 给它们。 如果你通过很多层不使用该数据的中间组件(并且只会向下传递)来传递数据,这通常意味着你在此过程中忘记了抽象组件。举个例子,你可能想传递一些像posts的数据 props 到不会直接使用这个参数的组件,类似<Layout posts={posts} />。取而代之的是,让Layout把children当做一个参数,然后渲染<Layout><Posts posts={posts} /></Layout>。这样就减少了定义数据的组件和使用数据的组件之间的层级。
如果这两种方法都不适合你,再考虑使用 context。
Context 的使用场景
- 主题: 如果你的应用允许用户更改其外观(例如暗夜模式),你可以在应用顶层放一个 context provider,并在需要调整其外观的组件中使用该 context。
- 当前账户: 许多组件可能需要知道当前登录的用户信息。将它放到 context 中可以方便地在树中的任何位置读取它。某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。
- 路由: 大多数路由解决方案在其内部使用 context 来保存当前路由。这就是每个链接“知道”它是否处于活动状态的方式。如果你创建自己的路由库,你可能也会这么做。
- 状态管理: 随着你的应用的增长,最终在靠近应用顶部的位置可能会有很多 state。许多遥远的下层组件可能想要修改它们。通常 将 reducer 与 context 搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。
Context 不局限于静态值。如果你在下一次渲染时传递不同的值,React 将会更新读取它的所有下层组件!这就是 context 经常和 state 结合使用的原因。
一般而言,如果树中不同部分的远距离组件需要某些信息,context 将会对你大有帮助。
使用context:
- 创建
import { createContext } from 'react';
export const isLargeImag = createContext(false)
- 包裹适当的上层组件
<isLargeImag.Provider value={imageSize}>
//
</isLargeImag.Provider>
- 在目标组件(需要使用的组件)
const isLarge = useContext(isLargeImag)
useRef
在一些不常见的情况下,你可能希望限制暴露的功能。你可以用 useImperativeHandle 做到这一点:
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 只暴露 focus,没有别的
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
Effect 在屏幕更新后的 提交阶段 运行
每当你的组件渲染时,React 将更新屏幕,然后运行 useEffect 中的代码。换句话说,useEffect 会把这段代码放到屏幕更新渲染之后执行
没有依赖数组作为第二个参数,与依赖数组位空数组 [] 的行为是不一致的:
useEffect(() => {
// 这里的代码会在每次渲染后执行(每次组件渲染都会执行)
});
useEffect(() => {
// 这里的代码只会在组件挂载后执行(只会在组件初始化时执行一次,销毁后再再次渲染还会执行)
}, []);
useEffect(() => {
//这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行 (只会在组件初始化都会执行一次)
}, [a, b]);
为什么依赖数组中可以省略 ref?
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);
这是因为 ref 具有 稳定 的标识:React 保证 每轮渲染中调用 useRef 所产生的引用对象时,获取到的对象引用总是相同的,即获取到的对象引用永远不会改变,所以它不会导致重新运行 Effect。因此,依赖数组中是否包含它并不重要。useState 返回的 set 函数 也有稳定的标识符,所以也可以把它从依赖数组中忽略掉。
如果 ref 是从父组件传递的,则必须在依赖项数组中指定它。这样做是合适的,因为无法确定父组件是否始终是传递相同的 ref,或者可能是有条件地传递几个 ref 之一。因此,你的 Effect 将取决于传递的是哪个 ref。
每次重新执行(effect非首次执行) Effect 之前,React 都会调用清理函数;组件被卸载时,也会调用清理函数。让我们看看执行清理函数会做些什么:
import { useState, useEffect } from 'react';
export default function ChatRoom() {
useEffect(() => {
console.log('执行')
//清理函数
return () => console.log('执行清理函数');
}, []);
return <h1>欢迎来到聊天室!</h1>;
}
组件第一次渲染时(strictMode):
输出:
//'执行'
//'执行清理函数'
//'执行'
如果effect有依赖项,依赖项变换后(初始化之后的effect执行),会先执行清除函数,清除上一次的effect,然后再重新运行effect;
输出:
//'执行清理函数'
//'执行'