React Hooks 核心原理与实战

React Hooks 核心原理与实战

Tags: 前端源码
创建时间: May 24, 2022
评星: ⭐️⭐️⭐️⭐️⭐️

03|内置 Hooks(1):如何保存组件状态和使用生命周期?

useState原则

state 中永远不要保存可以通过计算得到的值。

React Hooks 四种执行时机

useEffect 让我们能够在下面四种时机去执行一个回调函数产生副作用:

  • 每次 render 后执行:不提供第二个依赖项参数。比如useEffect(() => {})
  • 仅第一次 render 后执行:提供一个空数组作为依赖项。比如useEffect(() => {}, [])
  • 第一次以及依赖项发生变化后执行:提供依赖项数组。比如useEffect(() => {}, [deps])
  • 组件 unmount 后执行:返回一个回调函数。比如useEffect() => { return () => {} }, [])

Hooks 的使用规则

  • Hooks 只能在函数组件的顶级作用域使用

    所谓顶层作用域,就是 Hooks 不能在循环、条件判断或者嵌套函数内执行,而必须是在顶层。同时 Hooks 在组件的多次渲染之间,必须按顺序被执行。

  • Hooks 只能在函数组件或者其它 Hooks 中使用

04|内置 Hooks(2):为什么要避免重复定义回调函数?

useCallback:缓存回调函数

1
2
3
4
5
6
7
8
9
function Counter() {
const [count, setCount] = useState(0);

**// handleIncrement是定义在函数内部的,因此在多次渲染之间无法重用,每次都需要重新创建**
const handleIncrement = () => setCount(count + 1);

**// 每次创建新函数的方式会让接收事件处理函数的组件,需要重新渲染。**
return <button onClick={handleIncrement}>+</button>
}

接收了 handleIncrement ,并作为一个属性。如果每次都是一个新的,那么这个 React 就会认为这个组件的 props 发生了变化,从而必须重新渲染。因此,我们需要做到的是:只有当 count 发生变化时,我们才需要重新定一个回调函数。而这正是 useCallback 这个 Hook 的作用。

1
2
3
4
5
6
7
8
9
10
11
import React, { useState, useCallback } from 'react';

function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(
() => setCount(count + 1),
[count], // 只有当 count 发生变化时,才会重新创建回调函数
);
// ...
return <button onClick={handleIncrement}>+</button>
}

useMemo:缓存计算的结果

1
2
3
4
let usersToShow = null; 
if (users) { // 无论组件为何刷新,这里一定会对数组做一次过滤的操作
usersToShow = users.data.filter((user) => user.first_name.includes(searchKey), );
}

这个场景应该很容易理解:如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算。

1
2
3
4
5
6
7
8
9
//...
// 使用 userMemo 缓存计算的结果
const usersToShow = useMemo(() => {
if (!users) return null;
return users.data.filter((user) => {
return user.first_name.includes(searchKey));
}
}, [users, searchKey]);
//...

缓存上一次的计算结果,useMemo 可以避免子组件重复渲染。

useRef:在多次渲染之间共享数据

useRef 看作是在函数组件之外创建的一个容器空间,用于共享变量。

1
2
3
4
5
6
7
8
9
10
11

// 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
const timer = useRef(null);

// 开始计时的事件处理函数
const handleStart = useCallback(() => {
// 使用 current 属性设置 ref 的值
timer.current = window.setInterval(() => {
setTime((time) => time + 1);
}, 100);
}, []);

保存某个 DOM 节点的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

useContext:定义全局状态

要跨层次,或者同层的组件之间要进行数据的共享,React 提供了 Context 这样一个机制,能够让所有在某个组件开始的组件树上创建一个 Context。这样这个组件树上的所有组件,就都能访问和修改这个 Context 了。那么在函数组件里,我们就可以使用 useContext 这样一个 Hook 来管理 Context。

08|复杂状态处理:如何保证状态一致性?

原则一:保证状态最小化

1
2
3
4
5
6
7
8
9
10
11
12

const [searchKey, setSearchKey] = useState("");

**// 每当 searchKey 或者 data 变化的时候,重新计算最终结果**
**// 利用useMemo缓存计算的值**
const filtered = useMemo(() => {
return data.filter((item) =>
item.title.toLowerCase().includes(searchKey.toLowerCase())
);
}, [searchKey, data]);

// {filtered.map((item) => {}) ...

🔥 核心原则,如果能通过计算得到结果,就不要保存在state中。这样需要手动维护数据一致性,增加复杂度。

原则二: 避免中间状态,确保唯一数据源

10|函数组件设计模式:如何应对复杂条件渲染场景?

容器模式:实现按条件执行 Hooks

所谓容器模式,是将判断逻辑放在容器内部,从而避免在判断逻辑中写 hooks

1
2
3
4
5
6
7
8
9
10
// 定义一个容器组件用于封装真正的 UserInfoModal
export default function UserInfoModalWrapper({
visible,
...rest, // 使用 rest 获取除了 visible 之外的属性
}) {
// 如果对话框不显示,则不 render 任何内容
if (!visible) return null;
// 否则真正执行对话框的组件逻辑
return <UserInfoModal visible {...rest} />;
}

同理,在自定义hooks内部,判断是否处理

1
2
3
4
5
6
7
8
9
10
function useUser(id) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 当 id 不存在,直接返回,不发送请求
if (!id) return
// 获取用户信息的逻辑
});
}

使用 render props 模式重用 UI 逻辑

render props 就是把一个 render 函数作为属性传递给某个组件,由这个组件去执行这个函数从而 render 实际的内容。

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 { useState, useCallback } from "react";

function CounterRenderProps({ children }) {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
const decrement = useCallback(() => {
setCount(count - 1);
}, [count]);

return children({ count, increment, decrement });
}

function CounterRenderPropsExample() {
return (
<CounterRenderProps>
{({ count, increment, decrement }) => {
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}}
</CounterRenderProps>
);
}

一个showMore的例子,实现UI的重用

Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ListWithMore.jsx
import { Popover } from "antd";

function ListWithMore({ renderItem, data = [], max }) {
const elements = data.map((item, index) => renderItem(item, index, data));
const show = elements.slice(0, max);
const hide = elements.slice(max);
return (
<span className="exp-10-list-with-more">
{show}
{hide.length > 0 && (
<Popover content={<div style={{ maxWidth: 500 }}>{hide}</div>}>
<span className="more-items-wrapper">
and{" "}
<span className="more-items-trigger"> {hide.length} more...</span>
</span>
</Popover>
)}
</span>
);
}
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
// ListWithMoreExample.jsx 这里用一个示例数据
import data from './data';

function ListWithMoreExample () => {
return (
<div className="exp-10-list-with-more">
<h1>User Names</h1>
<div className="user-names">
Liked by:{" "}
**<ListWithMore**
renderItem={(user) => {
return <span className="user-name">{user.name}</span>;
}}
data={data}
max={3}
/>
</div>
<br />
<br />
<h1>User List</h1>
<div className="user-list">
<div className="user-list-row user-list-row-head">
<span className="user-name-cell">Name</span>
<span>City</span>
<span>Job Title</span>
</div>
**<ListWithMore**
renderItem={(user) => {
return (
<div className="user-list-row">
<span className="user-name-cell">{user.name}</span>
<span>{user.city}</span>
<span>{user.job}</span>
</div>
);
}}
data={data}
max={5}
/>
</div>
</div>
);
};