定时请求数据
该问题的原因是 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]   ); }
 
 
 
  |