React Hooks

Hooks 概览

定义变量
useState:定义普通变量
useReducer:定义有不同类型、参数的变量
组件传值
useContext():定义和接收具有全局性质的属性传值对象,必须配合 React.createContext()使用
对象引用
useRef:获取渲染后的 DOM 元素对象,可调用该对象原生 html 的方法,可能需要配合 React.forwardRef()使用
useImperativeHandle:获取和调用渲染后的 DOM 元素对象拥有的自定义方法,必须配合 React.forwardRef()使用
生命周期
useEffect:挂载或渲染完成后、即将被卸载前,调度

useLayoutEffect:浏览器把内容真正渲染到界面之前,同步调度
性能优化
useCallback:获取某处理函数的引用,必须配合 React.memo()使用
useMemo:获取某处理函数返回值的副本
代码调试
useDebugValue:对 react 开发调试工具中的自定义 hook,增加额外显示信息
自定义 hook
useCustomHook:将 hook 相关逻辑代码从组件中抽离,提高 hook 代码可复用性。

类组件转化为函数组件

  1. 创建一个与重构页面类组件同名的函数。
  2. return 进类组件中的页面结构。
  3. 类中定义的变量,可以使用一个状态变量去表示,使用 useState 钩子我们将把组件重写为一个函数。用 useState 钩子,来处理类中多状态值。
  4. 在函数组件中我们可以使用 useEffect 钩子去处理类组件中的生命周期方法
  5. 注意类组件转为函数组件以后功能的变化。

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//类组件
import React, { Component } from "react";

class App extends Component {
state = {
year: 1995,
type: "Mercedes",
used: true,
};
swapCar = () => {
this.setState({
year: 2018,
type: "BMW",
used: false,
});
};

render() {
return (
<div style={{ marginBottom: "50px" }}>
<h2>Challenge 3</h2>
<h3>Car Spec is:</h3>
<ul>
<li>{this.state.type}</li>
<li>{this.state.year}</li>
<li>{this.state.used ? "Used Car" : "Brand New!"}</li>
</ul>
<button onClick={this.swapCar}>Swap Car!</button>
</div>
);
}
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//函数组件重写
import React, { useState } from "react";

function App() {
const [year, changeYear] = useState(1995);
const [type, changeType] = useState("Mercedes");
const [used, changeCondition] = useState(true);

const swapCar = () => {
changeYear(2018);
changeType("BMW");
changeCondition(false);
};

return (
<div style={{ marginBottom: "50px" }}>
<h2>Challenge 3</h2>
<h3>Car Spec is:</h3>
<ul>
<li>{type}</li>
<li>{year}</li>
<li>{used ? "Used Car" : "Brand New!"}</li>
</ul>
<button onClick={swapCar}>Swap Car!</button>
</div>
);
}

export default App;

自定义 hook

相当于将复用逻辑封装到一个 hook 中.在多个函数复用同一个逻辑时使用,
在自定义 hook 的时候可以遵循一个简单原则:当参数大于 2 个的时候返回值的类型返回 object, 否则返回数组

  • 自定义 hook 其实是一个函数,但是要以 use 开头,函数内部可以调用 其他 hook
  • 自定义 hook 复用的是逻辑,而非状态
  • hook 只能用在函数组件中或者自定义 hook 里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const useInputValue = (initialValue) => {
const [value, setValue] = useState(initialValue);
return {
value,
onChange: (e) => setValue(e.target.value),
};
};

const TodoForm = () => {
const text = useInputValue("");
const password = useInputValue("");
return (
<form>
<input type="text" {...text} />
<input type="password" {...password} />
</form>
);
};
export default TodoForm;

独立性

多个函数调用相同的自定义 Hook,他们共用一个 state 吗?
答案是不是同一个,是独立的。
官网上讲:
每次调用 Hook,它都会获取独立的 state。由于我们直接调用了 useFriendStatus,从 React 的角度来看,我们的组件只是调用了 useState 和 useEffect。 正如我们在之前章节中了解到的一样,我们可以在一个组件中多次调用 useState 和 useEffect,它们是完全独立的。

useState

为函数引入状态的钩子.
useState 中可以是对象,数组,函数.
如果是函数,那么函数的返回值就是初始值.而不是保存函数的状态.函数只会被调用一次。这是惰性初始值.
set 方法是异步操作.

函数组件的 useState 修改 state 是直接覆盖。
类组件修改 state 是进行合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//在react中引入
import React,{ useState } from 'react';
//在function中设置
export default () => {
/**
* 使用一个数组包裹
* count: 状态
* setCount: 修改状态
* useState(0): 括号内的值是初始值
*/
const [count, setCount] = useState(0);
return (
<div>
{ count }
<button onClick={()=>{setCount(count+1)}>add</button>
</div>
)
}
// 如果将调用函数抽离,使用setCount时注意参数是count
const handleCount = () => {
setCount(count => count +1)
}

原则

如果一个值可以基于现有的 props 或 state 计算得出,不要把它作为一个 state,而是在渲染期间直接计算这个值.

函数式更新

1
<button onClick={()=> {setCount(count => count+1)}>+</button>
新的state通过上一个state计算得出,可以将函数传给setState. 该函数接收上一个state,返回一个新的state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
setCount(count + 1)
}, 3000);
}
function handleClickFn() {
setTimeout(() => {
setCount((prevCount) => {
return prevCount + 1
})
}, 3000);
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
当设置为异步更新,点击按钮延迟到3s之后去调用setCount函数,当快速点击按钮时,也就是说在3s多次去触发更新,但是只有一次生效,因为 count 的值是没有变化的。而当使用函数式更新 state 的时候,这种问题就没有了,因为它可以获取之前的 state 值,也就是代码中的 prevCount 每次都是最新的值。

其实这个特点和类组件中 setState 类似,可以接收一个新的 state 值更新,也可以函数式更新。如果新的 state 需要通过使用先前的 state 计算得出,那么就要使用函数式更新。因为 setState 更新可能是异步,当你在事件绑定中操作 state 的时候,setState 更新就是异步的。

一般操作 state,因为涉及到 state 的状态合并,react 认为当你在事件绑定中操作 state 是非常频繁的,所以为了节约性能 react 会把多次 setState 进行合并为一次,最后在一次性的更新 state,而定时器里面操作 state 是不会把多次合并为一次更新的。

惰性更新

使用函数传入 useState,这样将结果作为初始值,且只执行一次,后续更新也不再执行,节省开销。

1
2
const initialState = Number(window.localStorage.getItem("count"));
const [count, setCount] = React.useState(initialState);

当函数组件更新 re-render 时,函数组件内所有代码都会重新执行一遍。此时 initialState 的初始值是一个相对开销较大的 IO 操作。每次函数组件 re-render 时,第一行代码都会被执行一次,引起不必要的性能损耗。

1
2
const initialState = () => Number(window.localStorage.getItem("count"));
const [count, setCount] = React.useState(initialState);

如何初始化数据?
可以先声明一个变量,用它接收修改后的值,然后把它当做初始值传入 useState.

1
2
3
4
5
let list:IBox[] = []
for (let i = 0; i < 10; i++) {
list.push({ text: "content", boxRef:useRef<HTMLDivElement>(null)})
}
const [boxes, setBoxes] = useState<IBox[]>(list)

useEffect

useEffect 相当于三种状态
分别是挂载后,更新后,卸载前.

  • componentDidMount: useEffect(()=>{},[])
  • componentDidUpdate,componentDidMount: useEffect(()=>{})
  • componentWillUnmount: useEffect(()=>{ return ()=>{ //这里相当于 compoonentWillUnmount }},[])

和 componentDidMount 和 componentDidUpdate 的区别:
ueEffect 是在真实 DOM 构建之后执行,componentDidMount 和 componentDidUpdate 是在真实 DOM 构建之前执行。
原因:useEffect 执行时异步的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//useEffect使用

//class示例
import React, { Component } from "react";
export default class demo extends Component {
state = { count: 0 };
//想要在title上点击更新title
componentDidUpdate() {
document.title = `clicked ${this.state.count} times`;
}
render() {
return (
<div>
count: {`clicked ${this.state.count} times`}
<button
onClick={() => {
this.setState({
count: (this.state.count += 1),
});
}}
>
add
</button>
</div>
);
}
}

//hook示例
import React, { useState, useEffect } from "react";
export default () => {
const [count, setCount] = useState(0);

/**
* useEffect相当于三种状态
* componentDidMount: useEffect(()=>{},[])
* componentDidUpdate,componentDidMount: useEffect(()=>{})
* componentWillUnmount: useEffect(()=>{
* return ()=>{ //这里相当于compoonentWillUnmount }},[])
*/
useEffect(() => {
document.title = `clicked ${count} times`;
});
return (
<div>
count: {`clicked ${count} times`}
<button onClick={() => setCount(count + 1)}>add</button>
</div>
);
};

useEffect 挂载时执行两次

这是 React18 在严格模式做的提醒,注意清除副作用。

useEffect 结合异步函数

需要将异步函数改为自执行的形式,因为 useEffect 返回清理资源的函数,而异步函数返回 Promise,
或者在回调内部创建一个异步函数.

为什么不能直接使用 async/await 呢?
原因: useEffect 的返回值是用来清除副作用的,执行操作应该是可预期的,而不应该是一个异步函数.
所以异步函数应该在回调函数内部.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建异步函数,并在useEffect中执行
useEffect(() =>{
const asyncFn = async () => {}
asyncFn()
},[])


// 使用自执行函数
useEffect(()=> {
(async () => {
await axios.get()
})()
}

useEffect 副作用

纯函数

  • 纯函数的输出和输入值以外的其他信息无关
  • 纯函数不能有可观察的函数副作用

副作用

  1. 引用外部变量
  2. 调用外部函数
  3. 只要不是在组件渲染时用到的变量,所有的操作都是副作用

副作用指函数在执行过程中,除了返回可能的函数值外,还对主调用函数产生附加的影响.

例如:修改了全局变量、修改了传入的参数、甚至是 console.log(), ajax 操作,直接修改 DOM,计时器函数,其他异步操作,其他会对外部产生影响的操作都是算作副作用

我们知道,如果在 useEffect 函数中返回一个函数,这个函数就是清除副作用函数,它会在组件销毁的时候执行,但是其实,它会在组件每次重新渲染时执行,并且先执行清除上一个 effect 的副作用。

规则

当我们编写组件时,应该尽量将组件编写为纯函数。
对于组件中的副作用,首先应该明确:

是**用户行为触发的**还是**视图渲染后主动触发的**?
对于前者,将逻辑放在Event handlers中处理。 对于后者,使用useEffect处理。 具体例子看下面可能不需要使用useEffect

执行时机

  1. 在组件重新 render()后,先执行清除副作用,后执行 useEffect 其他的。
  2. 在组件销毁时执行。
1
2
3
4
5
// 存在清理函数
// 1. render + 清理函数
// => render + 清理函数 + useEffect
// => render + 清理函数 + useEffect
// 组件销毁时执行。

注意上面第一条中,在 render()后执行清除副作用,也就是会先渲染新的 UI,再清除上一次的副作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
假设第一次渲染的时候props是{id: 10},第二次渲染的时候是{id: 20}。
你可能会认为发生了下面的这些事:

React 清除了 {id: 10}的effect。
React 渲染{id: 20}的UI
React 运行{id: 20}的effect。
(事实并不是这样。)

React只会在浏览器绘制后运行effects。
这使得你的应用更流畅因为大多数effects并不会阻塞屏幕的更新。
Effect的清除同样被延迟了。上一次的effect会在重新渲染后被清除:

React 渲染{id: 20}的UI
浏览器绘制。我们在屏幕上看到{id: 20}的UI
React 清除{id: 10}的effect。
React 运行{id: 20}的effect。

原因:
组件内的每一个函数(包括事件处理函数,effects,定时器或者API调用等等)
会捕获定义它们的那次渲染中的props和state。

effect的清除并不会读取“最新”的props。它只能读取到定义它的那次渲染中的props值



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { Component } from "react";
const MyAPI = {
count: 0,
subscribe(cb) {
this.intervalId = setInterval(() => {
this.count += 1;
cb(this.count);
}, 1000);
},
unSubscribe() {
clearInterval(this.intervalId);
},
reset() {
this.count = 0;
},
};
//class写法
export default class demo extends Component {
state = {
count: 0,
};
//设置定时器
componentDidMount() {
MyAPI.subscribe((count) => {
this.setState({ count: count });
});
}
//清除定时器
componentWillUnmount() {
MyAPI.unSubscribe();
}
render() {
return <div>{this.state.count}</div>;
}
}
//Hook写法
export default () => {
const [count, setCount] = useState(0);

//设置定时器
useEffect(() => {
MyAPI.subscribe((currentCount) => {
setCount(currentCount);
});
//使用清除副作用函数,清除定时器
return () => {
MyAPI.unSubscribe();
};
}, []);
return <div>{count}</div>;
};

依赖项

  1. 依赖项是数组[],在初次渲染和组件卸载时执行
  2. 有依赖项,且依赖项不一致时执行

依赖的类型

如果是 true,0,null,{},会不会更新?
这个原始类型和引用类型,跟 Object.is 比较有关系。
原始类型是不会变化的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default function App() {
const [count, setCount] = useState(0);
const [initState, setInitState] = useState({});
// 通过Object.is比较发现initState是同一个对象引用,所以不更新
useEffect(() => {
console.log("useEffect");
}, [initState]);
// 这里是不同的对象引用,那么就会更新
useEffect(() => {
console.log("useEffect");
}, [{}]);
// 这里值不会变化,所以不更新
useEffect(() => {
console.log("useEffect");
}, [true]);

return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>click</button>
</div>
);
}

useEffect 定时器问题

为什么开发模式下,计时器每次多打印+2?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useState, useEffect } from "react";

export default function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
function onTick() {
setCount((c) => c + 1);
}

setInterval(onTick, 1000);
}, []);

return <h1>{count}</h1>;
}

开发模式下,为了提示要记得清除副作用,每个组件都会重复挂载一次.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
const [count, setCount] = useState(0);
console.log("render", count);
useEffect(() => {
console.log("useEffect", count);
const id = setInterval(() => {
console.log("interval", count);
setCount(count + 1);
}, 5000);
return () => {
console.log("clear", count);
clearInterval(id)
};
}, []);

return <h1>{count}</h1>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

// 解释
render => useEffect => interval =>setCount(1)
render => 因为依赖项为空,则不再执行useEffect。
但是内部的定时器还会执行。但是定时器拿到的count是第一次的count0,每次打印都是1
对比count没有变化 不再render




const [count, setCount] = useState(0);
// 使用传入函数的方式,保证拿到上一次的参数
// 这个定时器只有一个
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
// 将count放入依赖项,会每次运行生成一个定时器
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
Ï }, [count]);

使用setState的函数形式,实现当前值的+1.

尽管 effect 只运行了一次,第一次渲染中的定时器回调函数可以完美地在每次触发的时候给 React 发送 c => c + 1 更新指令。它不再需要知道当前的 count 值。因为 React 已经知道了。

useLayoutEffect,和 useEffect 区别?在哪个阶段执行? beginWork,commit Work

二者的函数签名是相同的,不同点在于触发的时机不同.
useLayoutEffect是同步处理副作用.useEffect 是异步处理.
useLayoutEffect在 DOM 更新后,浏览器渲染之前,useLayoutEffect内部的更新将会同步刷新.
useEffect是在浏览器渲染后.
useLayoutEffect是在commitWork阶段.

useLayoutEffect

想在 useLayoutEffect 中实现 constructor()的类似方法,也就是在页面渲染前初始化数据,

1
2
3
4
5
6
7
8
9
10
11
12
13
const [boxes, setBoxes] = useState([]);
useLayoutEffect(() => {
let list = [];
for (let i = 0; i < 10; i++) {
list.push({ text: "content", boxRef });
}
setBoxes(list);
}, []);
useEffect(() => {
// 这里获取上面初始化后的boxes,可以修改了
console.log(boxes);
//
}, []);

useHook 与闭包

闭包陷阱

hooks 的闭包陷阱是指 useEffect 等 hook 中用到了某个 state,但是没有把它加到 deps 数组里,导致 state 变了,但是执行的函数依然引用着之前的 state

解决方法

它的解决方式就是正确设置 deps 数组,把用到的 state 放到 deps 数组里,这样每次 state 变了就能执行最新的函数,引用新的 state。同时要清理上次的定时器、事件监听器等。
useRef 能解决闭包陷阱的原因是 useEffect 等 hook 里不直接引用 state,而是引用 ref.current,这样后面只要修改了 ref 中的值,这里取出来的就是最新的。

解决 hooks 的闭包陷阱有两种方式:

  • 设置依赖的 state 到 deps 数组中并添加清理函数
  • 不直接引用 state,把 state 放到 useRef 创建的 ref 对象中再引用

处理定时器的时候,为保证计时的准确,最好使用 useRef 的方式,其余情况两种都可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`); // 0,1,2,3...
}, 3000);
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

// 对比class组件
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);// 5,5,5,5,5
}, 3000);
}

上面两个对比,uesEffect 因为有闭包存在,记录的都是当时 count 的值.
class 组件里,React 修改了 class 中的 this.state 使其指向最新状态.所以全都是 5.

**每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state。**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 先点击show按钮,在快速点击增加count,会弹出显示多少?
function Counter() {
const [count, setCount] = useState(0);

function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
// 弹出 0,handleAlertClick 的运行环境里,state 的 count 是 0,setTimeout 里也是 0,只不过延迟了

````

```jsx
export default function App() {
const [name, setName] = useState("");
const [show, setShow] = useState(false);
// 场景1,更改input,点击button,输出什么?
useEffect(() => {
if (show) {
console.log('name1',name);
}
}, [show]);
// 场景2,修改input内容,5秒后,输出什么?
useEffect(() => {
setTimeout(() => {
console.log('name2',name);
}, 5000);
}, []);

return (
<div className="App">
<input type="text" onChange={(e) => setName(e.target.value)} />
<button onClick={() => setShow(true)}>alert</button>
</div>
);
}
````

总结:
场景 1:依赖项是 show,show 改变时的 state 中,name 是已经改变的值。
场景 2:定时器里使用是 state 是初次挂载时的 state,也就是空。

```tsx
const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
// 挂载前
const oldTitle = document.title;
// 挂载时,传入的title赋给文档的title,而且oldTitle也改变了
useEffect(() => {
document.title = title;
}, []);
// 卸载时
useEffect(() => {
return () => {
if (!keepOnUnmount) {
// 因为这里面是一个闭包, 所以里面的oldTitle还是最初的title
// 如果不指定依赖,这里的闭包就让title是旧title
document.title = oldTitle;
}
};
}, []);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 案例
function App() {
const [count, setCount] = useState(0);
const [countTimeout, setCountTimeout] = useState(0);

useEffect(() => {
setTimeout(() => {
setCountTimeout(count);
}, 3000);
setCount(5);
}, []);

return (
//count发生了变化,但是3s后setTimout的count却还是0
<div>
Count: {count}
setTimeout Count: {countTimeout}
</div>
);
}

执行顺序

mount 顺序:父 => 子
effect 顺序:子 => 父
clean 顺序:子 => 父
如果想 effect 在父组件先执行,可以使用 useLayoutEffect.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { useState, useEffect } from "react";

function Child() {
console.log("child mount");
useEffect(() => {
console.log("child effect");
return () => {
console.log("child clean");
};
}, []);

return <p>hello</p>;
}

function Parent() {
console.log("parent mount");
useEffect(() => {
console.log("parent effect");
return () => {
console.log("parent clean");
};
}, []);

return <Child />;
}

function App() {
console.log("App mount");
useEffect(() => {
console.log("App effect");
return () => {
console.log("App clean");
};
}, []);

return <Parent />;
}
export default App;
// 打印顺序
// App mount 2
// parent mount 2
// child mount
// child effect
// parent effect
// App effect
// child clean
// parent clean
// App clean
// child effect
// parent effect
// App effect

为什么会执行两次 effect,因为 React18 的并发模式会强制让组件更新一次,也就是先 clean=>effect。

竞态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import React, { useState, useEffect } from "react";

function Article({ id }) {
const [article, setArticle] = useState(null);

useEffect(() => {
let didCancel = false;
console.log("effect", didCancel);

function fetchData() {
console.log("setArticle begin", didCancel);
new Promise((resolve) => {
setTimeout(() => {
console.log("setTimeout", didCancel);
resolve(id);
}, id);
}).then((article) => {
// 快速点击 Add id 的 button,这里 didCancel 为什么会打印 true
console.log("setArticle end", didCancel, article);
if (!didCancel) {
// 把这一行代码注释就会出现错误覆盖状态值的情况
setArticle(article);
}
});
}
console.log("fetchData begin", didCancel);
fetchData();
console.log("fetchData end", didCancel);

return () => {
didCancel = true;
console.log("clear", didCancel);
};
}, [id]);

return <div>{article}</div>;
}

function App() {
const [id, setId] = useState(5000);
function handleClick() {
setId(id - 1000);
}
return (
<>
<button onClick={handleClick}>add id</button>
<Article id={id} />
</>
);
}

export default App;

快速连点两次,为什么 didCancel 是 true?
自己理解:记住,组件内部的函数每次执行都会捕获当时运行时的 props 和 state。

  1. 第一次运行时,didCancel 开始是 false,执行请求 fetchData,如果请求没来得及返回,还是 false。

但是,

  1. 注意,在还没返回时,又执行了 useEffect,就会先执行上一次的清除副作用。把上一次的 cancel 改为 true。

上一次的 fetchData 请求返回结果时,已经变成了 true。就不在执行 setArticle()了。

解释: 第一次点击,4 秒 setTimeout,页面刷新,渲染 3 秒的 UI,调用 4 秒的移除副作用,设置 4 秒的 didCancel 为 true.setTimeout 中的 didCancel 就是此时的 true
.第二次点击,3 秒 setTimeout,页面没有卸载或更新,didCancel 还是 false.
这里主要是 effect 移除副作用是在第二次 ui 渲染后,再移除上一次的 effect 副作用.因为它会捕获记住那一次的 props 和 state.setTimeout 中记录的也是上一次的.

class 组件解决竞态

1
2
3
4
5
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}

闭包陷阱示例

本质还是 useEffect 内部的 v 还是挂载时捕获的 state,input 改变,事件处理函数拿到的还是第一次的 state。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const btn = useRef();
const [v, setV] = useState("");

useEffect(() => {
let clickHandle = () => {
console.log("v:", v);
};
btn.current.addEventListener("click", clickHandle);

return () => {
btn.current.removeEventListener("click", clickHandle);
};
}, [v]);

const inputHandle = (e) => {
setV(e.target.value);
};

return (
<>
<input value={v} onChange={inputHandle} />
<button ref={btn}>测试</button>
</>
);

产生原因

能访问到的自由变量 v 此时还是空值。当点击事件触发时,执行点击回调函数,此时先创建执行上下文,会拷贝作用域链到执行上下文中。

如果未在输入框内输入字符,此时点击拿到的 v 还是原来那个 v
如果在输入框内输入了字符,此时调用了 setV 修改了 state,页面触发 render,组件内部代码会重新执行一遍,重新声明了一个 v,v 就不再是原来那个 v,这里点击事件里作用域中的 v 还是旧的 v,这是两个不同的 v

产生场景

  • 事件绑定。比如示例代码中,在页面最初渲染完成后只绑定一次事件的情况,比如使用 echarts,在 useEffect 中获取 echarts 的实例并绑定事件
  • 定时器。页面加载后注册一个定时器,定时器内的函数也会产生如此的闭包问题。

解决办法

  1. 使用useRef替代useState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const btn = useRef();
const vRef = useRef("");
const [v, setV] = useState("");

useEffect(() => {
let clickHandle = () => {
console.log("v:", vRef.current);
};
btn.current.addEventListener("click", clickHandle);

return () => {
btn.current.removeEventListener("click", clickHandle);
};
}, []);

const inputHandle = (e) => {
let { value } = e.target;
vRef.current = value;
setV(value);
};

return (
<>
<input value={v} onChange={inputHandle} />
<button ref={btn}>测试</button>
</>
);

useRef 的方案之所以有效,是因为每次 input 的 change 修改的是 vRef 这个对象的 current 属性,而 vRef 始终是那个 vRef,即使 rerender,由于 vRef 是对象,所以变量存储在栈内存中的值是该对象在堆内存中的地址,只是一个引用,只修改对象的某个属性,该引用并不会改变。所以点击事件中的作用域链始终访问的都是同一个 vRef

  1. useEffect的依赖加上v.

方法虽然有用,但是每次 v 更新都会重新渲染,重新注册点击事件.只希望注册一次事件,所以也不是非常好.

Hook 规则

不要在循环,条件,嵌套中使用 Hook,确保它在最顶层.
只在 React 函数中调用 Hook,不要在 js 函数中调用.

1
2
3
4
5
6
7
8
9
10
11
12
//错误方式
if (count === 0) {
useEffect(() => {
document.title = `clicked ${count} times`;
});
}
//正确方式
useEffect(() => {
if (count === 0) {
document.title = `clicked ${count} times`;
}
});

Event 和 Effect

事件处理函数内部的逻辑是非响应式的。除非用户又执行了同样的操作(例如点击),否则这段逻辑不会再运行。事件处理函数可以在“不响应”他们变化的情况下读取响应式值。
Effect 内部的逻辑是响应式的。如果 Effect 要读取响应式值,你必须将它指定为依赖项。如果接下来的重新渲染引起那个值变化,React 就会使用新值重新运行 Effect 内的逻辑。

移除 Effect 依赖

每次有新的消息到达时,这个 Effect 会用新创建的数组更新 messages state:
它使用 messages 变量来 创建一个新的数组:从所有现有的消息开始,并在最后添加新的消息。然而,由于 messages 是一个由 Effect 读取的响应式值,它必须是一个依赖:

1
2
3
4
5
6
7
8
9
10
11
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ 所有依赖已声明
// ...

而让 messages 成为依赖会带来问题。

每当你收到一条消息,setMessages() 就会使该组件重新渲染一个新的 messages 数组,其中包括收到的消息。然而,由于该 Effect 现在依赖于 messages,这 也将 重新同步该 Effect。所以每条新消息都会使聊天重新连接。用户不会喜欢这样!

为了解决这个问题,不要在 Effect 里面读取 messages。相反,应该将一个 state 更新函数 传递给 setMessages:

1
2
3
4
5
6
7
8
9
10
11
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ 所有依赖已声明
// ...

可能不需要使用 useEffect

当 props 变化重置 state

ProfilePage 组件接收一个 prop:userId。页面上有一个评论输入框,你用了一个 state:comment 来保存它的值。有一天,你发现了一个问题:当你从一个人的个人资料导航到另一个时,comment 没有被重置。这导致很容易不小心把评论发送到不正确的个人资料。为了解决这个问题,你想在 userId 变化时,清除 comment 变量:
但这是低效的,因为 ProfilePage 和它的子组件首先会用旧值渲染,然后再用新值重新渲染。并且这样做也很复杂,因为你需要在 ProfilePage 里面 所有 具有 state 的组件中都写这样的代码。例如,如果评论区的 UI 是嵌套的,你可能也想清除嵌套的 comment state。

取而代之的是,你可以通过为每个用户的个人资料组件提供一个明确的键来告诉 React 它们原则上是 不同 的个人资料组件。将你的组件拆分为两个组件,并从外部的组件传递一个 key 属性给内部的组件:

1
2
3
4
5
6
7
8
9
export default function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}

function Profile({ userId }) {
// ✅ 当 key 变化时,该组件内的 comment 或其他 state 会自动被重置
const [comment, setComment] = useState("");
// ...
}

通常,当在相同的位置渲染相同的组件时,React 会保留状态。通过将 userId 作为 key 传递给 Profile 组件,使 React 将具有不同 userId 的两个 Profile 组件视为两个不应共享任何状态的不同组件。每当 key(这里是 userId)变化时,React 将重新创建 DOM,并 重置 Profile 组件和它的所有子组件的 state。现在,当在不同的个人资料之间导航时,comment 区域将自动被清空。

当 prop 变化时调整部分 state

有时候,当 prop 变化时,你可能只想重置或调整部分 state ,而不是所有 state。

List 组件接收一个 items 列表作为 prop,然后用 state 变量 selection 来保持已选中的项。当 items 接收到一个不同的数组时,你想将 selection 重置为 null:

1
2
3
4
5
6
7
8
9
10
11
// ❌ 错误示例
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// 🔴 避免:当 prop 变化时,在 Effect 中调整 state
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}

这不太理想。每当 items 变化时,List 及其子组件会先使用旧的 selection 值渲染。然后 React 会更新 DOM 并执行 Effect。最后,调用 setSelection(null) 将导致 List 及其子组件重新渲染,重新启动整个流程。

让我们从删除 Effect 开始。取而代之的是在渲染期间直接调整 state:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不太合适的示例
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// 好一些:在渲染期间调整 state
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}

像这样 存储前序渲染的信息 可能很难理解,但它比在 Effect 中更新这个 state 要好。上面的例子中,在渲染过程中直接调用了 setSelection。当它执行到 return 语句退出后,React 将 立即 重新渲染 List。此时 React 还没有渲染 List 的子组件或更新 DOM,这使得 List 的子组件可以跳过渲染旧的 selection 值。

在渲染期间更新组件时,React 会丢弃已经返回的 JSX 并立即尝试重新渲染。为了避免非常缓慢的级联重试,React 只允许在渲染期间更新 同一 组件的状态。如果你在渲染期间更新另一个组件的状态,你会看到一条报错信息。条件判断 items !== prevItems 是必要的,它可以避免无限循环。你可以像这样调整 state,但任何其他副作用(比如变化 DOM 或设置的延时)应该留在事件处理函数或 Effect 中,以 保持组件纯粹。

虽然这种方式比 Effect 更高效,但大多数组件也不需要它。无论你怎么做,根据 props 或其他 state 来调整 state 都会使数据流更难理解和调试。总是检查是否可以通过添加 key 来重置所有 state,或者 在渲染期间计算所需内容。例如,你可以存储已选中的 item ID 而不是存储(并重置)已选中的 item:

1
2
3
4
5
6
7
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ 非常好:在渲染期间计算所需内容
const selection = items.find((item) => item.id === selectedId) ?? null;
// ...
}

现在完全不需要 “调整” state 了。如果包含已选中 ID 的项出现在列表中,则仍然保持选中状态。如果没有找到匹配的项,则在渲染期间计算的 selection 将会是 null。行为不同了,但可以说更好了,因为大多数对 items 的更改仍可以保持选中状态。

在事件处理函数共享逻辑

假设你有一个产品页面,上面有两个按钮(购买和付款),都可以让你购买该产品。当用户将产品添加进购物车时,你想显示一个通知。在两个按钮的 click 事件处理函数中都调用 showNotification() 感觉有点重复,所以你可能想把这个逻辑放在一个 Effect 中
这个 Effect 是多余的。而且很可能会导致问题。例如,假设你的应用在页面重新加载之前 “记住” 了购物车中的产品。如果你把一个产品添加到购物车中并刷新页面,通知将再次出现。每次刷新该产品页面时,它都会出现。这是因为 product.isInCart 在页面加载时已经是 true 了,所以上面的 Effect 每次都会调用 showNotification()。

当你不确定某些代码应该放在 Effect 中还是事件处理函数中时,先自问 为什么 要执行这些代码。Effect 只用来执行那些显示给用户时组件 需要执行 的代码
在这个例子中,通知应该在用户 按下按钮 后出现,而不是因为页面显示出来时!删除 Effect 并将共享的逻辑放入一个被两个事件处理程序调用的函数中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
function ProductPage({ product, addToCart }) {
// ✅ 非常好:事件特定的逻辑在事件处理函数中处理
function buyProduct() {
addToCart(product);
showNotification(`已添加 ${product.name} 进购物车!`);
}

function handleBuyClick() {
buyProduct();
}

function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}

````

## 发送post请求
并不是请求必须在effect中.

例子:
这个 Form 组件会发送两种 POST 请求。它在页面加载之际会发送一个分析请求。当你填写表格并点击提交按钮时,它会向 /api/register 接口发送一个 POST 请求:

让我们应用与之前示例相同的准则。
分析请求应该保留在 Effect 中。这是 因为 发送分析请求是表单显示时就需要执行的(在开发环境中它会发送两次,请 参考这里 了解如何处理)。

然而,发送到 /api/register 的 POST 请求并不是由表单 显示 时引起的。你只想在一个特定的时间点发送请求:当用户按下按钮时。它应该只在这个 特定的交互 中发生。删除第二个 Effect,将该 POST 请求移入事件处理函数中:
```jsx
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ 非常好:这个逻辑应该在组件显示时执行
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);

function handleSubmit(e) {
e.preventDefault();
// ✅ 非常好:事件特定的逻辑在事件处理函数中处理
post('/api/register', { firstName, lastName });
}
// ...
}
````

当你决定将某些逻辑放入事件处理函数还是 Effect 中时,你需要回答的主要问题是:从用户的角度来看它是 怎样的逻辑。如果这个逻辑是由某个特定的交互引起的,请将它保留在相应的事件处理函数中。如果是由用户在屏幕上 看到 组件时引起的,请将它保留在 Effect 中。

## 链式调用

有时候你可能想链接多个 Effect,每个 Effect 都基于某些 state 来调整其他的 state:
更好的做法是:尽可能在渲染期间进行计算,以及在事件处理函数中调整 state:

```tsx
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);

// ✅ 尽可能在渲染期间进行计算
const isGameOver = round > 5;

function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('游戏已经结束了。');
}

// ✅ 在事件处理函数中计算剩下的所有 state
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('游戏结束!');
}
}
}
}

通知父组件有关 state 变化的信息

假设你正在编写一个有具有内部 state isOn 的 Toggle 组件,该 state 可以是 true 或 false。有几种不同的方式来进行切换(通过点击或拖动)。你希望在 Toggle 的 state 变化时通知父组件,因此你暴露了一个 onChange 事件并在 Effect 中调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);

function updateToggle(nextIsOn) {
// ✅ 非常好:在触发它们的事件中执行所有更新
setIsOn(nextIsOn);
onChange(nextIsOn);
}

function handleClick() {
updateToggle(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
updateToggle(true);
} else {
updateToggle(false);
}
}

// ...
}

通过这种方式,Toggle 组件及其父组件都在事件处理期间更新了各自的 state。React 会 批量 处理来自不同组件的更新,所以只会有一个渲染流程。

你也可以完全移除该 state,并从父组件中接收 isOn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 也很好:该组件完全由它的父组件控制
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}

// ...
}

“状态提升” 允许父组件通过切换自身的 state 来完全控制 Toggle 组件。这意味着父组件会包含更多的逻辑,但整体上需要关心的状态变少了。每当你尝试保持两个不同的 state 变量之间的同步时,试试状态提升

将数据传递给父组件

既然子组件和父组件都需要相同的数据,那么可以让父组件获取那些数据,并将其 向下传递 给子组件:

1

useMemo

useMemo 起作用的前提是,该组件必须是浅比较.
行为类似于 Vue 中的计算属性.防止内部因 js 属性导致无限循环.
另外当 obj 是对象的 state 时,不会无限循环.

1
2
3
4
5
import { useMemo } from "react";
const result = useMemo(() => {
//如果count变化则此函数重新执行
return result;
}, [count]);

案例: 是否能拿到 num1 最新的值?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function App() {
return <Demo1 />;
}

function Demo1() {
const [num1, setNum1] = useState(1);
const [num2, setNum2] = useState(10);

const text = useMemo(() => {
return `num1: ${num1} | num2:${num2}`;
}, [num2]);

function handClick() {
setNum1(2);
setNum2(20);
}

return (
<div>
{text}
<div>
<button onClick={handClick}>click!</button>
</div>
</div>
);
}

useMemo 中依赖的数组,作用是判断是否要执行回调函数。
上面虽然只是依赖了 num2,但是 num2 变化,同样执行了回调,num1 也就同样拿到了最新的值。

memo 方法

用于性能优化,节省开销.,如果本组件内数据未变化,阻止组件更新.
类似于 React.pureComponent,该函数会浅比较 props,state 是否变化,未变化就不重复渲染.
React.memo 有第二个参数。可以比较深层次的 props。表示是否相等, 返回值是布尔值。为 true 就不 re-render.

1
2
3
4
5
function arePropsEqual(prevProps, nextProps) {
return prevPRops === nextProps;
}

export default memo(Button, arePropsEqual);

与 shouldComponentUpdate 不同的是,arePropsEqual 返回 true 时,不会触发 render。返回 false 会触发。
而 shouldComponentUpdate 正好相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { PureComponent } from "react";
import Child from "./child";
export default class demo extends PureComponent {
state = {
time: new Date(),
};
componentDidMount() {
setInterval(() => {
//当time设置为1时,即使渲染还是1,但是log还是会打印render.
//使用PureComponent可以浅比较发现数据没有变化,就不会继续打印.
// this.setState({ time: 1},1000)
this.setState({ time: new Date() }, 1000);
});
}

render() {
console.log("render");
return (
<div>
{/* 加入child组件 */}
{/* 当赋值没有变化时,依旧会渲染log */}
<Child seconds={1} />
{this.state.time.toString()}
</div>
);
}
}

//Child组件
const Child = ({ seconds }) => {
console.log("child");
return <div>current time: {seconds}</div>;
};
//加入memo防止组件重复渲染
export default React.memo(Child);

useCallback

返回一个 memoized 回调函数。
useCallback 只是根据依赖是否变化,返回新的函数或者是之前缓存的函数。
依旧是节省资源.
被 useCallback 包裹的函数是否执行,取决于后面的参数是否变化.
如果后面参数没有执行,而且在第一次的时候时可以执行一次的.

当写自定义 hook 时,返回的函数大概率需要用 useCallback 包裹.返回的函数保证不会每次都变化.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useState, useCallback } from "react";
export default () => {
const [count1, setCount1] = useState(0);
const [count, setCount] = useState(0);
//useCallback第二个参数决定是否允许第一个参数执行
//如果count发生变化则允许执行
//useCallback第一个参数会先执行一次,之后才判断第二个参数是否变化
return (
<div>
<p>{count1}</p>
<button onClick={() => setCount(count1 + 1)}>click</button>
<p>{count}</p>
<button onClick={useCallback(() => setCount(count + 1), [count])}>
click
</button>
</div>
);
};

useCallback 和 useEffect

复用请求逻辑时,可以 使用 useCallback,或者自定义 hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 1. 把公共逻辑放到组件外

// 你不再需要把它设为依赖,因为它们不在渲染范围内,
// 因此不会被数据流影响。它不可能突然意外地依赖于props或state。
// ✅ Not affected by the data flow
function getFetchUrl(query) {
return "https://hn.algolia.com/api/v1/search?query=" + query;
}

function SearchResults() {
useEffect(() => {
const url = getFetchUrl("react");
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK

useEffect(() => {
const url = getFetchUrl("redux");
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK

// ...
}

// 2. 使用useCallback

// useCallback本质上是添加了一层依赖检查。
// 它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖
function SearchResults() {
// ✅ Preserves identity when its own deps are the same
const getFetchUrl = useCallback(
(query) => {
return "https://hn.algolia.com/api/v1/search?query=" + query;
},
[query]
); // ✅ Callback deps are OK

useEffect(() => {
const url = getFetchUrl("react");
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK

useEffect(() => {
const url = getFetchUrl("redux");
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK

// ...
}

// 3. 请求属性从父级传入

function Parent() {
const [query, setQuery] = useState("react");

// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = "https://hn.algolia.com/api/v1/search?query=" + query;
// ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK

return <Child fetchData={fetchData} />;
}

function Child({ fetchData }) {
let [data, setData] = useState(null);

useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK

// ...
}

在 class 组件中,函数属性本身并不是数据流的一部分。组件的方法中包含了可变的 this 变量导致我们不能确定无疑地认为它是不变的。因此,即使我们只需要一个函数,我们也必须把一堆数据传递下去仅仅是为了做“diff”。我们无法知道传入的 this.props.fetchData 是否依赖状态,并且不知道它依赖的状态是否改变了。

使用 useCallback,函数完全可以参与到数据流中。我们可以说如果一个函数的输入改变了,这个函数就改变了。如果没有,函数也不会改变。感谢周到的 useCallback,属性比如 props.fetchData 的改变也会自动传递下去。

useCallback 可以用 useMemo 实现

1
2
3
useCallback(fn, []);

useMemo(() => fn, []);

useMemo 和 useCallback 准则

  1. 大部分的 useMemo 和 useCallback 都应该被移除,他们可能没有带来性能上的优化,反而增加首次渲染的负担,增加程序的复杂性。
  2. 使用 useMemo/useCallback 优化子组件 re-render 时,必须满足以下条件才有效。
    1. 子组件已通过 React.memo/useMemo 被缓存
    2. 子组件所有 props 被缓存

如何判断是否应该缓存组件?

  1. react dev tools profiler
  2. useRenderTImes

什么时候应该用 useMemo/useCallback?

  1. 防止不必要的 effect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Component = () => {
const a = useMemo(() => ({ test: 1 }), []);
useEffect(() => {
// 只有当a变化了,这里才触发
doSomething();
}, [a]);
};

const Component = () => {
const fetch = useCallback(() => {
// fetch数据
}, []);

useEffect(() => {
// 仅fetch值被改变时,才会触发
fetch();
}, [fetch]);
};
  1. 防止不必要的 re-render
    1. 组件什么时候会 re-render
      1. 本身的 props 或者 state 改变时
      2. Context value 改变,使用该值的组件会 re-render
      3. 当父组件重新渲染,所有的子组件都会 re-render
1
2
3
4
5
6
7
8
9
10
11
12
// 必须同时缓存组件本身和onClick方法
const PageMemoized = React.memo(Page);

const App = () => {
const [state, setState] = useState(0);

const onClick = useCallback(() => {
//dosomething
}, []);

return <PageMemorized onClick={onClick} />;
};
  1. 防止不必要的重复计算

useReducer

useReducer 是另一种返回状态的方法.可以说是useState的升级版本.和 redux 的差别在于并不能进行全局的状态管理.
useState 适合单个状态,useReducer 适合多个互相有影响的状态.
对于单个状态,可以使用 useState,多个状态,且互相关联,就可以使用 useReducer.

它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

假如层级过深,还可以搭配 context 使用,此时使用 dispatch 代替 callback 优势更明显。因为 dispatch 在 re-render 时不变,不会引起使用 context 的组件执行无意义的更新。
批量更新 react 对事件之外的更新不会批量处理,使用 reducer 可以避免此类问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useReducer } from "react";

const initalState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "dcrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
export default () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "dcrement" })}>-</button>
</div>
);
};

useReducer 与 useEffect

使用 useReducer 进行解耦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React, { useReducer, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

// 相比如使用两个state在useEffect中
useEffect(() => {
const id = setInterval(() => {
setCount((c) => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
// 上面的方案会在修改step后重新创建定时器,如果想不再创建可以使用useReducer解耦

// 使用useReducer更合适
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" });
}, 1000);
return () => clearInterval(id);
}, []);

return (
<>
<h1>{count}</h1>
<input
value={step}
onChange={(e) => {
dispatch({
type: "step",
step: Number(e.target.value),
});
}}
/>
</>
);
}

const initialState = {
count: 0,
step: 1,
};

function reducer(state, action) {
const { count, step } = state;
if (action.type === "tick") {
return { count: count + step, step };
} else if (action.type === "step") {
return { count, step: action.step };
} else {
throw new Error();
}
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

useReducer 依赖 props 时

我们可以把 reducer 函数放到组件内去读取 props
这种模式会使一些优化失效,所以你应该避免滥用它,不过如果你需要你完全可以在 reducer 里面访问 props。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Counter({ step }) {
const [count, dispatch] = useReducer(reducer, 0);

function reducer(state, action) {
if (action.type === "tick") {
return state + step;
} else {
throw new Error();
}
}

useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);

return <h1>{count}</h1>;
}

即使是在这个例子中,React 也保证 dispatch 在每次渲染中都是一样的。 所以你可以在依赖中去掉它。它不会引起 effect 不必要的重复执行。

你可能会疑惑:这怎么可能?在之前渲染中调用的 reducer 怎么“知道”新的 props?答案是当你 dispatch 的时候,React 只是记住了 action - 它会在下一次渲染中再次调用 reducer。在那个时候,新的 props 就可以被访问到,而且 reducer 调用也不是在 effect 里。

这就是为什么我倾向认为 useReducer 是 Hooks 的“作弊模式”。它可以把更新逻辑和描述发生了什么分开。结果是,这可以帮助我移除不必需的依赖,避免不必要的 effect 调用。

useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext providercontext value
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider>来为下层组件提供 context

createContext

1
const context = createContext(defaultValue);

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React, { useContext } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
//先创建一个Context
const AppContext = React.createContext({});

const Navbar = () => {
const { username } = useContext(AppContext);

return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
};

const Messages = () => {
const { username } = useContext(AppContext);

return (
<div className="messages">
<h1>Messages</h1>
<p>1 message for {username}</p>
<p className="message">useContext is awesome!</p>
</div>
);
};
//用创建的Context包裹组件
function App() {
return (
<AppContext.Provider
value={{
username: "superawesome",
}}
>
<div className="App">
<Navbar />
<Messages />
</div>
</AppContext.Provider>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

全局使用

创建 context

使用 createContext 方法创建 context

1
2
3
4
5
6
7
8
9
10
11
12
// 泛型可以约束传入的内容
const AuthContext = createContext<
| {
user: User | null;
login: (form: AuthForm) => Promise<void>;
register: (form: AuthForm) => Promise<void>;
logout: () => void;
}
| undefined
>(undefined);
// context 的名字
AuthContext.displayName = "AuthContext";

创建 Provider 及其附属方法

1
2
3
4
5
6
7
8
9
10
11
12
13
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);

const login = (form: AuthForm) => auth.login(form).then(setUser);
const register = (form: AuthForm) => auth.register(form).then(setUser);
const logout = () => auth.logout();
return (
<AuthContext.Provider
children={children}
value={{ user, login, register, logout }}
/>
);
};

使 Context 在全局生效

包裹 App 组件

1
2
3
4
5
6
7
root.render(
<React.StrictMode>
<AppProviders>
<App />
</AppProviders>
</React.StrictMode>
);

调用 context

使用 useHook 自定义 hook 调用 context

1
2
3
4
5
6
7
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth必须在AuthProvider中使用");
}
return context;
};

页面中使用

1
const { login, user } = useAuth();

useRef

类组件、React 元素用 React.createRef,函数组件使用 useRef
useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue).

第二个作用是保存数据
即使组件重新渲染,保存的数据仍然存在.保存的数据被更改不会触发组件渲染.

useRef 是在 memorizedState 链表中放一个对象,current 保存某个值。
初始化的时候创建了一个对象放在 memorizedState 上,后面始终返回这个对象。

这样通过 useRef 保存回调函数,然后在 useEffect 里从 ref.current 来取函数再调用,避免了直接调用,也就没有闭包陷阱的问题了。

何时使用 ref

通常,当你的组件需要“跳出” React 并与外部 API 通信时,你会用到 ref —— 通常是不会影响组件外观的浏览器 API。以下是这些罕见情况中的几个:

  1. 存储 timeout ID
  2. 存储和操作 DOM 元素
  3. 存储不需要被用来计算 JSX 的其他对象。

如果你的组件需要存储一些值,但不影响渲染逻辑,请选择 ref。

注意:
使用 useRef 创建 div 元素时,如果存在循环创建,一定要把创建写在里面,不要在外面创建一个然后引用,因为引用的都是同一个

1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误❌
const boxRef = useRef < HTMLDivElement > null;

let list: IBox[] = [];
for (let i = 0; i < 10; i++) {
list.push({ text: "content", boxRef }); // 这里是10个一样的
}
// 正确✅
let list: IBox[] = [];
for (let i = 0; i < 10; i++) {
list.push({ text: "content", boxRef: useRef < HTMLDivElement > null });
// 这里是10个就是不一样的
}
1
2
3
4
5
6
7
8
9
10
11
12
const fn = () => {
console.log(count);
};
const ref = useRef(fn);

useLayoutEffect(() => {
ref.current = fn;
});

useEffect(() => {
setInterval(() => ref.current(), 500);
}, []);

useEffect 里执行定时器,deps 设置为了 [],所以只会执行一次,回调函数用的是 ref.current,没有直接依赖某个 state,所以不会有闭包陷阱。
用 useRef 创建个 ref 对象,初始值为打印 count 的回调函数,每次 render 都修改下其中的函数为新创建的函数,这个函数里引用的 count 就是最新的。
这里用了 useLayoutEffect 而不是 useEffect 是因为 useLayoutEffect 是在 render 后同步执行的,useEffect 是在 render 后异步执行的,所以用 useLayoutEffect 能保证在 useEffect 之前被调用。
这种方式避免了 useEffect 里直接对 state 的引用,从而避免了闭包问题。
另外,修改 count 的地方,可以用 setCount(count => count + 1) 代替 setCount(count + 1),这样也就避免了闭包问题

1
const refContainer = useRef(initialValue);

useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的 ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
function Parent() {
let [number, setNumber] = useState(0);
return (
<>
<Child />
<button onClick={() => setNumber({ number: number + 1 })}>+</button>
</>
);
}
let input;
function Child() {
const inputRef = useRef();
console.log("input===inputRef", input === inputRef);
input = inputRef;
function getFocus() {
inputRef.current.focus();
}
return (
<>
<input type="text" ref={inputRef} />
<button onClick={getFocus}>获得焦点</button>
</>
);
}
ReactDOM.render(<Parent />, document.getElementById("root"));

useRef 使用的细节

  1. 函数组件能否使用 createRef?

可以但是最好不要用。createRef 主要是 class 组件访问 dom 的,最佳实践是整个生命周期只用一次,一般在构造函数中。如果在函数组件中使用会造成每次 render 都会调用 createRef。

  1. 每次渲染 useRef 返回值不变
  2. ref.current 发生变化不会造成 re-render。
  3. ref.current 发生变化应该作为 side effect(因为它会影响下次渲染)。所以不应该在 render 阶段更新 current 属性。
  4. 修改 ref.current 的值不能另外赋值再修改,必须修改 ref.current。这和指针有关。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function App() {
const RenderCounter = () => {
const counter = useRef(0);

const handler = () => {
counter.current = counter.current + 1;
console.log(counter.current); // 虽然打印123,但是没有re-render,页面始终是0
//所以应该在useEffect中写
}

return (
<div>
<button onClick={handler}>add</button>
<h1>{`The component has been re-rendered ${counter.current} times`}</h1>
</div>
);
}
return <RenderCounter />

函数回调获取 ref(ref callback)

ref 属性除了接受 ref 对象之外,还可以接受函数也就是 ref callback。在该函数中,DOM 元素作为其唯一参数。
与 effect 函数一样,React 在组件周期中的某些时刻中调用它。当创建 DOM 元素之后会立即执行 ref callback(参数是 DOM 元素),在删除元素时也会再次调用 ref callback,只不过这时的参数是 null。
如果 ref callback 被定义为内联函数,React 将在每次渲染时调用它两次,第一次的参数是 null,第二次的参数是 DOM 元素。
虽然内联 ref callback 被调用两次可能会令人惊讶,如果从 React 的角度来看,我认为这种行为是合理的。它保证了一致性,因为每次渲染都会创建新的函数实例,它可能是一个完全不同的函数。这些函数可能会依赖 props 或 state,而这些 props 或 state 也可能在此期间发生了变化。
因此 React 需要清除旧的 ref callback(参数是 null),然后设置新的回调(参数是 DOM 元素)。这样我们可以根据条件来设置 ref 属性的值,甚至在 React 元素之间交换它们

1
2
3
4
5
6
7

function Child() {
let inputRef;

return (
<>
<input type="text" ref={dom =>{ inputRef = dom}} />
1
2
3
4
5
6
7
const [size, setSize] = useState();

const measureRef = useCallback((node) => {
setSize(node.getBoundingClientRect());
}, []);

return <div ref={measureRef}>{children}</div>;

在这个案例中,没有选择使用 useRef,因为当 ref 是一个对象时,它并不会把当前 ref 值的变化情况通知到我们。使用 callback ref 可以确保即便被测量的节点在子组件延迟显示 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

useRef 案例

案例: 主要利用 ref 保存了计时器的设置函数,因为在 useEffect 中,每次计算都会重新计算,如果不加 count 依赖,就不再重新计时,需要使用 ref 将计时器方法保存.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//
import React, { useState, useEffect, useRef } from "react";
import "./style.css";

export default function App() {
const [open, setOpen] = useState(false);
const [count, setCount] = useState(0);
const ref = useRef();

useEffect(() => {
ref.current = () => {
setCount(count + 1);
};
});

useEffect(() => {
let timer = null;
if (open) {
console.log(open, "timer open");
timer = setInterval(() => ref.current(), 1000);
}
if (!open) {
console.log(open);
}
return () => {
clearInterval(timer);
console.log(open, "卸载");
};
}, [open]);

const toggleModal = () => {
if (!open) {
setOpen(true);
} else {
setOpen(false);
}
};
const Modal = () => {
return <div style={{ display: open ? "block" : "none" }}>模态框</div>;
};
return (
<div>
<div>{count}</div>
<button onClick={toggleModal}>打开模态框</button>
<Modal />
</div>
);
}

forwardRef

接收两个参数,一个是 props,一个是 ref.

1
2
3
4
5
6
7
8
9
10
//因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性
function Parent() {
return (
<>
// <Child ref={xxx} /> 这样是不行的
<Child />
<button>+</button>
</>
);
}
  • forwardRef 可以在父组件中操作子组件的 ref 对象
  • forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上
  • 子组件接受 props 和 ref 作为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Child(props, ref) {
return <input type="text" ref={ref} />;
}
Child = React.forwardRef(Child);
function Parent() {
let [number, setNumber] = useState(0);
// 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
// 只有当它被赋给某个元素的 ref 属性时,才会有值
// 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
// 那么父组件就可以操作子组件中的某个元素
// 但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的
// 所以就需要用到 forwardRef 进行转发
const inputRef = useRef(); //{current:''}
function getFocus() {
inputRef.current.value = "focus";
inputRef.current.focus();
}
return (
<>
<Child ref={inputRef} />
<button onClick={() => setNumber({ number: number + 1 })}>+</button>
<button onClick={getFocus}>获得焦点</button>
</>
);
}

useImperativeHandle

  • useImperativeHandle 可以让你在使用 ref 时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛
  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
  • 父组件可以使用操作子组件中的多个 ref
  • 使用场景: 父组件调用子组件方法

作用: 将父级的 ref 拦截,把回调函数返回的值赋给父级的 ref.达到了自定义暴露给父组件的目的.

参数有三个: ref , 回调函数, 依赖项[].

1
2
3
function useImperativeHandle(ref, cb) {
ref.current = cb();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';

function Child(props,parentRef){
// 子组件内部自己创建 ref
let focusRef = useRef();
let inputRef = useRef();
useImperativeHandle(parentRef,()=>{
// 这个函数会返回一个对象
// 该对象会作为父组件 current 属性的值
// 通过这种方式,父组件可以使用操作子组件中的多个 ref
return {
//注意这里是返回对象,必须有{}
focusRef,
inputRef,
name:'计数器',
focus(){
focusRef.current.focus();
},
changeText(text){
inputRef.current.value = text;
}
}
}};
return (
<>
<input ref={focusRef}/>
<input ref={inputRef}/>
</>
)

}
// 子组件需要forwardRef包裹
ForwardChild = forwardRef(Child);

function Parent(){
const parentRef = useRef();//{current:''}
function getFocus(){
parentRef.current.focus();
// 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效
parentRef.current.addNumber(666);
parentRef.current.changeText('<script>alert(1)</script>');
console.log(parentRef.current.name);
}
return (
<>
// 子组件上绑定父组件设置的ref
<ForwardChild ref={parentRef}/>
<button onClick={getFocus}>获得焦点</button>
</>
)
}

useTransition

用于提升性能,一般用在大量数据展示时,频繁修改请求参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useState, useTransition } from "react";
const Home: React.FC = () => {
const [val, setVal] = useState("");
const [arr, setArr] = useState<number[]>([]);
// pending 就是确定当前状态是否是已经设置的,可搭配loading
const [pending, transition] = useTransition();

const getList = (e: any) => {
setVal(e.target.value);
let arr = Array.from(new Array(10000), (item) => Date.now());
// 这里transition可以将数据减缓,相当于debounce
transition(() => {
setArr(arr);
});
};
return (
<div className={styles.box}>
<input value={val} onChange={getList} />
{pending ? (
<h2>loading....</h2>
) : (
arr.map((item, key) => <div key={key}>{item}</div>)
)}
</div>
);
};

export default Home;

新增 hook

useEffectEvent

试验性特性引入

1
import { experimental_useEffectEvent as useEffectEvent } from "react";

提取非响应式逻辑

例如,假设你想在用户连接到聊天室时展示一个通知。并且通过从 props 中读取当前 theme(dark 或者 light)来展示对应颜色的通知
当 roomId 变化时,聊天会和预期一样重新连接。但是由于 theme 也是一个依赖项,所以每次你在 dark 和 light 主题间切换时,聊天 也会 重连。这不是很好!

1
2
3
4
5
6
7
8
9
10
11
12
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ 声明所有依赖项
// ...

使用 useEffectEvent 这个特殊的 Hook 从 Effect 中提取非响应式逻辑

这里的 onConnected 被称为 Effect Event。它是 Effect 逻辑的一部分,但是其行为更像事件处理函数。它内部的逻辑不是响应式的,而且能一直“看见”最新的 props 和 state。

现在你可以在 Effect 内部调用 onConnected Effect Event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ChatRoom({ roomId, theme }) {
// Effect Event
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 声明所有依赖项
// ...

Effect Event 是非响应式的并且必须从依赖项中删除。

接收参数

effect event 是非响应式的逻辑,接收的参数就是一个事件调用的对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import { useState, useEffect, useRef } from "react";
import { FadeInAnimation } from "./animation.js";
import { experimental_useEffectEvent as useEffectEvent } from "react";

function Welcome({ duration }) {
const ref = useRef(null);

const onAppear = useEffectEvent((animation) => {
animation.start(duration);
});

useEffect(() => {
const animation = new FadeInAnimation(ref.current);
// 接收参数animation
onAppear(animation);
return () => {
animation.stop();
};
}, []);

return (
<h1
ref={ref}
style={{
opacity: 0,
color: "white",
padding: 50,
textAlign: "center",
fontSize: 50,
backgroundImage:
"radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)",
}}
>
欢迎
</h1>
);
}

export default function App() {
const [duration, setDuration] = useState(1000);
const [show, setShow] = useState(false);

return (
<>
<label>
<input
type="range"
min="100"
max="3000"
value={duration}
onChange={(e) => setDuration(Number(e.target.value))}
/>
淡入 interval: {duration} ms
</label>
<button onClick={() => setShow(!show)}>{show ? "移除" : "显示"}</button>
<hr />
{show && <Welcome duration={duration} />}
</>
);
}

局限性

Effect Event 的局限性在于你如何使用他们:

只在 Effect 内部调用他们。
永远不要把他们传给其他的组件或者 Hook。