定时请求数据
该问题的原因是 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 路由,所以走通配路由。
解决方法:
- 不使用 useEffect,使用 useLayoutEffect,或者动态生成路由是同步代码。
- 数据过多可能会出现卡顿,不如在页面渲染前单独准备一个 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 = () => { 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 />, }, { 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 = []) { const { current } = useRef({ fn, timer: null });
useEffect(() => { current.fn = fn; }, [fn]); 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 = []) { const { current } = useRef({ fn, timer: null });
useEffect(() => { current.fn = fn; }, [fn]); return useCallback( (...args) => { if (!current.timer) { current.timer = setTimeout(() => { delete current.timer; }, delay); current.fn.call(this, ...args); } }, [dep] ); }
|