Hooks实际应用

定时请求数据

该问题的原因是 Promise 执行过程中,无法取消后续还没有定义的 setTimeout() 导致的。所以最开始想到的就是我们不应该直接对 timeoutID 进行记录,而是应该向上记录整个逻辑的 Promise 对象。当 Promise 执行完成之后我们再清除 timeout,保证我们每次都能确切的清除掉任务。

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

export function useFetchDataInterval(fetchData) {
const [list, setList] = useState([]);
useEffect(() => {
let getListPromise;
async function getList() {
const data = await fetchData();
setList((list) => list.concat(data));
return setTimeout(() => {
getListPromise = getList();
}, 2000);
}

getListPromise = getList();
return () => {
getListPromise.then((id) => clearTimeout(id));
};
}, [fetchData]);
return list;
}

上面的方案能比较好的解决问题,但是在组件卸载的时候 Promise 任务还在执行,会造成资源的浪费。其实我们换个思路想一下,Promise 异步请求对于组件来说应该也是副作用,也是需要”清除“的。只要清除了 Promise 任务,后续的流程自然不会执行,就不会有这个问题了。
清除 Promise 目前可以利用 AbortController 来实现,我们通过在卸载回调中执行 controller.abort() 方法,最终让代码走到 Reject 逻辑中,阻止了后续的代码执行。

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

function fetchDataWithAbort({ fetchData, signal }) {
if (signal.aborted) {
return Promise.reject("aborted");
}
return new Promise((resolve, reject) => {
fetchData().then(resolve, reject);
signal.addEventListener("aborted", () => {
reject("aborted");
});
});
}
function useFetchDataInterval(fetchData) {
const [list, setList] = useState([]);
useEffect(() => {
let id;
const controller = new AbortController();
async function getList() {
try {
const data = await fetchDataWithAbort({
fetchData,
signal: controller.signal,
});
setList((list) => list.concat(data));
id = setTimeout(getList, 2000);
} catch (e) {
console.error(e);
}
}
getList();
return () => {
clearTimeout(id);
controller.abort();
};
}, [fetchData]);

return list;
}

上面一种方案,我们的本质是让异步请求抛错,中断了后续代码的执行。那是不是我设置一个标记变量,标记是非卸载状态才执行后续的逻辑也可以呢?所以该方案应运而生。
定义了一个 unmounted 变量,如果在卸载回调中标记其为 true。在异步任务后判断如果 unmounted === true 的话就不走后续的逻辑来实现类似的效果。

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

export function useFetchDataInterval(fetchData) {
const [list, setList] = useState([]);
useEffect(() => {
let id;
let unmounted;
async function getList() {
const data = await fetchData();
if (unmounted) {
return;
}

setList((list) => list.concat(data));
id = setTimeout(getList, 2000);
}
getList();
return () => {
unmounted = true;
clearTimeout(id);
};
}, [fetchData]);

return list;
}

Count To 动态数字

使用自定义 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
const useCount = (initialValue: number) => {
const [count, setCount] = useState(0);
const maxTimes: number = 200;
const interval: number = initialValue / maxTimes;

useEffect(() => {
let i = 0;
let timer: number | undefined;
if (initialValue <= maxTimes) {
setCount(initialValue);
return;
}
timer = setInterval(() => {
if (i >= maxTimes) {
clearInterval(timer);
setCount(initialValue);
return;
}
setCount((prev) => Math.floor(prev + interval));
i++;
}, 15);
return () => clearInterval(timer);
}, []);
return [count];
};

路由问题

问题描述:先匹配 404 页面,再匹配其他页面,会闪一下 404,再匹配其他。
问题原因:useEffect 在页面渲染后执行。
详细解释:动态路由生成放在 useEffect 中,那么是在页面渲染后才生成。渲染的时候没有匹配到 home 路由,所以走通配路由。
解决方法:

  1. 不使用 useEffect,使用 useLayoutEffect,或者动态生成路由是同步代码。
  2. 数据过多可能会出现卡顿,不如在页面渲染前单独准备一个 home 路由。其他路由还是异步。
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
const Router = () => {
// 从store获取路由表json,该json在登录时由请求接口获取
const routes = useBoundStore((state) => state.routes);
const [children, setChildren] = useState<RouteObject[]>([]);
// 根据路由表生成子路由
useEffect(() => {
const router = getRoutes(routes);
console.log("router: ", router);
setChildren(router);
}, [routes]);

// 生成路由
const rootRoutes: RouteObject[] = [
{
path: "/",
element: <Navigate to="/home" />,
},
{
path: "/",
element: <BasicLayout />,
children: children,
},
{
path: "/login",
element: <Login />,
// errorElement: <ErrorPage />,
},
{
path: "*",
element: <NotFound />,
},
];

const router = useRoutes(rootRoutes);

return router;
};

Keep-Alive

缓存 dom.

使用场景

  • 移动端中,用户访问了一个列表页,上拉浏览列表页的过程中,随着滚动高度逐渐增加,数据也将采用触底分页加载的形式逐步增加,列表页浏览到某个位置,用户看到了感兴趣的项目,点击查看其详情,进入详情页,从详情页退回列表页时,需要停留在离开列表页时的浏览位置上
  • 已填写但未提交的表单、管理系统中可切换和可关闭的功能标签等,这类数据随着用户交互逐渐变化或增长,这里理解为状态,在交互过程中,因为某些原因需要临时离开交互场景,则需要对状态进行保存

在 React 中,我们通常会使用路由去管理不同的页面,而在切换页面时,路由将会卸载掉未匹配的页面组件,所以上述列表页例子中,当用户从详情页退回列表页时,会回到列表页顶部,因为列表页组件被路由卸载后重建了,状态被丢失。

useHook 实现防抖和节流

useDebounce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 防抖
function useDebounce(fn, delay, dep = []) {
// 使用ref保存fn和timer
const { current } = useRef({ fn, timer: null });

useEffect(() => {
current.fn = fn;
}, [fn]);
// 使用useCallback保证返回的函数不会一直重渲染
return useCallback(
(...args) => {
if (current.timer) {
clearTimeout(current.timer);
}
current.timer = setTimeout(() => {
current.fn.call(this, ...args);
}, delay);
},
[dep]
);
}

useThrottle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 节流
function useThrottle(fn, delay, dep = []) {
// 使用ref保存fn和timer
const { current } = useRef({ fn, timer: null });

useEffect(() => {
current.fn = fn;
}, [fn]);
// 使用useCallback保证返回的函数不会一直重渲染
return useCallback(
(...args) => {
if (!current.timer) {
current.timer = setTimeout(() => {
delete current.timer;
}, delay);
current.fn.call(this, ...args);
}
},
[dep]
);
}

// 这个方法缺点,第一次会先执行一次