我从 37.2k star 的 react-use 库学到了这些

react-use hooks 目录分类

Pasted image 20230618094533.png

Sensors

  • useMouse(ref)

使用说明:传入目标元素,返回当前鼠标的系列坐标(如:鼠标文档横纵轴坐标,目标元素文档横纵轴坐标,相对于目标元素横纵轴坐标…)

const useMouse = (ref: RefObject<Element>): State => {
  if (process.env.NODE_ENV === 'development') {
    if (typeof ref !== 'object' || typeof ref.current === 'undefined') {
      console.error('useMouse expects a single ref argument.');
    }

  }




  // useRafState 是类似 useState 的版本,但使用了 rAF (requestAnimationFrame)
  // setState 时确保每60帧刷新间隙,更新 state
  const [state, setState] = useRafState<State>({
    docX: 0,
    docY: 0,
    posX: 0,
    posY: 0,
    elX: 0,
    elY: 0,
    elH: 0,
    elW: 0,
  });




  useEffect(() => {
    const moveHandler = (event: MouseEvent) => {
      if (ref && ref.current) {
        const { left, top, width: elW, height: elH } = ref.current.getBoundingClientRect();
        // pageXOffset, window.scrollX 的别名,即横向滚动距离
        // pageYOffset, window.scrollY 的别名,即纵向滚动距离
        const posX = left + window.pageXOffset;
        const posY = top + window.pageYOffset;
        const elX = event.pageX - posX;
        const elY = event.pageY - posY;


        setState({
          docX: event.pageX, // 鼠标事件横轴坐标(包含滚动)
          docY: event.pageY, // 鼠标事件纵轴坐标(包含滚动)
          posX, // ref 引用元素左上角横轴坐标(包含滚动)
          posY, // ref 引用元素左上角纵轴坐标(包含滚动)
          elX, // 鼠标事件相对于 ref 引用元素横轴坐标
          elY, // 鼠标事件相对于 ref 引用元素横轴坐标
          elH, // ref 引用元素高度
          elW, // ref 引用元素宽度
        });
      }
    };

    // 监听 mousemove 事件
    on(document, 'mousemove', moveHandler);
    // 移除监听
    return () => {
      off(document, 'mousemove', moveHandler);
    };
  }, [ref]);

  return state;
};
  • useEvent(name[, handler[, target[, options]]])

使用说明:监听某事件,触发事件回调,卸载时自动移除监听(目标/事件第三参数可配置-如捕获or冒泡阶段)

// interface isListenerType1 {
//     addEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
//     removeEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
// }
// UseEventTarget = isListenerType1 | isListenerType2




const useEvent = <T extends UseEventTarget>(
  name: Parameters<AddEventListener<T>>[0], // 监听事件名称
  handler?: null | undefined | Parameters<AddEventListener<T>>[1], // 事件handler
  target: null | T | Window = defaultTarget, // 监听目标对象
  options?: UseEventOptions<T> // 其余参数设置
) => {
  useEffect(() => {
    if (!handler) {
      return;


    }


    if (!target) {
      return;
    }
    if (isListenerType1(target)) {
      on(target, name, handler, options);
    } else if (isListenerType2(target)) {
      target.on(name, handler, options);
    }
    return () => {
      if (isListenerType1(target)) {
        off(target, name, handler, options);
      } else if (isListenerType2(target)) {
        target.off(name, handler, options);
      }
    };
  }, [name, handler, target, JSON.stringify(options)]);
};
  • useKey(key, handler, options, deps)

使用说明:监听键盘(默认为keydown)事件,触发事件回调,卸载时自动移除监听;本质上是封装使用了 useEvent

export type KeyPredicate = (event: KeyboardEvent) => boolean;
export type KeyFilter = null | undefined | string | ((event: KeyboardEvent) => boolean);
export type Handler = (event: KeyboardEvent) => void;



export interface UseKeyOptions<T extends UseEventTarget> {
  event?: 'keydown' | 'keypress' | 'keyup';
  target?: T | null;
  options?: UseEventOptions<T>;
}



const createKeyPredicate = (keyFilter: KeyFilter): KeyPredicate =>
  typeof keyFilter === 'function'
    ? keyFilter
    : typeof keyFilter === 'string'
    ? (event: KeyboardEvent) => event.key === keyFilter
    : keyFilter
    ? () => true
    : () => false;
    

const useKey = <T extends UseEventTarget>(
  key: KeyFilter, // key 可以是 string,也可以是函数
  fn: Handler = noop, // 监听 key 按键事件 handler
  opts: UseKeyOptions<T> = {}, // 自定义配置按键事件类型,监听目标元素,其余设置
  deps: DependencyList = [key]
) => {
  const { event = 'keydown', target, options } = opts;
  const useMemoHandler = useMemo(() => {
    const predicate: KeyPredicate = createKeyPredicate(key);
    const handler: Handler = (handlerEvent) => {
      if (predicate(handlerEvent)) {
        return fn(handlerEvent);
      }

    };

    return handler;
  }, deps);
  useEvent(event, useMemoHandler, target, options);
};

State

  • createGlobalState([initialState])

使用说明:利用闭包思想,创建了一个共享的 store;createGlobalState([initialState])返回一个数组 – [state, stateSetter],stateSetter 的调用由共享 store 控制

export function createGlobalState<S>(initialState?: S) {
  const store: {
    state: S;
    setState: (state: IHookStateSetAction<S>) => void;
    setters: any[];
  } = {
    state: initialState instanceof Function ? initialState() : initialState,
    setState(nextState: IHookStateSetAction<S>) {
      // resolveHookState 的作用是处理 nextState 是函数类型和普通类型的情况
      store.state = resolveHookState(nextState, store.state);
      store.setters.forEach((setter) => setter(store.state));
    },
    setters: [],
  };

  return () => {
    const [globalState, stateSetter] = useState<S | undefined>(store.state);
    // store.setState 更新操作遍历调用了 stateSetter,因此会触发所有关联的 hook 更新
    // store.setters 数组会移除'上一次绑定'调用过的 stateSetter
    useEffectOnce(() => () => {
      store.setters = store.setters.filter((setter) => setter !== stateSetter);
    });

	// 浏览器端于使用 useLayoutEffect,其余情况为 useEffect
    useIsomorphicLayoutEffect(() => {
      // stateSetter 不在 store.setters 数组则添加
      if (!store.setters.includes(stateSetter)) {
        store.setters.push(stateSetter);
      }
    });


    return [globalState, store.setState];
  };
}
  • createMemo(fn)

使用说明:接收一个函数,返回一个 memo 过的函数(函数调用结果会缓存效果)

const createMemo =
  <T extends (...args: any) => any>(fn: T) =>
  (...args: Parameters<T>) =>
    useMemo<ReturnType<T>>(() => fn(...args), args);

例子,斐波那契数列

import {createMemo} from 'react-use';




const fibonacci = n => {
  if (n === 0) return 0;
  if (n === 1) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2);
};



const useMemoFibonacci = createMemo(fibonacci);



const Demo = () => {
  const result = useMemoFibonacci(10);



  return (
    <div>
      fib(10) = {result}
    </div>
  );
};

  • useRafState(initialState)
 const useRafState = <S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] => {
  const frame = useRef(0);
  const [state, setState] = useState(initialState);



  // 相当于对 useState 进行了二次封装,保持每60帧 setState 更新
  const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
    cancelAnimationFrame(frame.current);



    frame.current = requestAnimationFrame(() => {
      setState(value);
    });
  }, []);



  useUnmount(() => {
    cancelAnimationFrame(frame.current);
  });


  return [state, setRafState];
};

  • useFirstState()

使用说明:利用 ref 不变性,记录 hook 是否为首次挂载

export function useFirstMountState(): boolean {
  const isFirst = useRef(true);




  if (isFirst.current) {
    isFirst.current = false;




    return true;
  }



  return isFirst.current;
}

Side Effects

  • useAsyncFn(fnReturnPromise[, deps[, initialState]])

使用说明:传入一个返回 Promise 的异步函数,返回 [函数调用状态,useCallback包装待调用函数]

export default function useAsyncFn<T extends FunctionReturningPromise>(
  fn: T,

  deps: DependencyList = [],
  initialState: StateFromFunctionReturningPromise<T> = { loading: false }
): AsyncFnReturn<T> {
  const lastCallId = useRef(0);
  const isMounted = useMountedState();
  const [state, set] = useState<StateFromFunctionReturningPromise<T>>(initialState);



  const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
    const callId = ++lastCallId.current;



    if (!state.loading) {
      set((prevState) => ({ ...prevState, loading: true }));
    }
    
    return fn(...args).then(
      (value) => {
        isMounted() && callId === lastCallId.current && set({ value, loading: false });




        return value;
      },
      (error) => {
        isMounted() && callId === lastCallId.current && set({ error, loading: false });

        return error;
      }

    ) as ReturnType<T>;
  }, deps);


  return [state, callback as unknown as T];
}
  • useAsync(fnReturnPromise[, deps])

使用说明:传入一个返回 Promise 的异步函数,异步函数在 useEffect 执行,返回函数调用的状态

export default function useAsync<T extends FunctionReturningPromise>(
  fn: T,

  deps: DependencyList = []
) {
  const [state, callback] = useAsyncFn(fn, deps, {
    loading: true,
  });



  useEffect(() => {
    callback();
  }, [callback]);



  return state;
}

  • useAsyncRetry(fnReturnPromise[, deps])

使用说明:useAsyncRetry 这里很巧妙的给 useAync 增加了一个 attempt 依赖,暴露的 retry 方法只需要更新 attempt,即可再次触发 useAsync 的调用(即异步函数的调用)

const useAsyncRetry = <T>(fn: () => Promise<T>, deps: DependencyList = []) => {
  const [attempt, setAttempt] = useState<number>(0);
  // !!!注意依赖数组添加了 attempt
  const state = useAsync(fn, [...deps, attempt]);


  const stateLoading = state.loading;
  const retry = useCallback(() => {
    if (stateLoading) {
      if (process.env.NODE_ENV === 'development') {
        console.log(
          'You are calling useAsyncRetry hook retry() method while loading in progress, this is a no-op.'
        );
      }

      return;


    }


    // retry 调用会更新 attempt
    setAttempt((currentAttempt) => currentAttempt + 1);
  }, [...deps, stateLoading]);




  return { ...state, retry };
};
  • useFavicon(href)

使用说明:这个很简单,即通过 useEffect 副作用调用更新网页 icon

const useFavicon = (href: string) => {
  useEffect(() => {

    const link: HTMLLinkElement =
      document.querySelector("link[rel*='icon']") || document.createElement('link');
    link.type = 'image/x-icon';
    link.rel = 'shortcut icon';
    link.href = href;
    document.getElementsByTagName('head')[0].appendChild(link);
  }, [href]);
};

UI

  • useClickAway(ref, handler[, events])

使用说明:传入指定元素,和事件监听(默认监听 mousedown/touchstart 事件),点击指定元素以外的区域,触发监听回调

const defaultEvents = ['mousedown', 'touchstart'];




const useClickAway = <E extends Event = Event>(
  ref: RefObject<HTMLElement | null>,
  onClickAway: (event: E) => void,
  events: string[] = defaultEvents
) => {
  const savedCallback = useRef(onClickAway);
  
  useEffect(() => {
    savedCallback.current = onClickAway;
  }, [onClickAway]);
  
  useEffect(() => {
    const handler = (event) => {
      const { current: el } = ref;
      el && !el.contains(event.target) && savedCallback.current(event);
    };
    

    for (const eventName of events) {
      on(document, eventName, handler);
    }
    
    return () => {
      for (const eventName of events) {
        off(document, eventName, handler);
      }

    };
  }, [events, ref]);
};
  • useFullscreen(ref, enabled [,options])

使用说明:传入需要全屏的元素,是否全屏设置,返回全屏的状态(true/false)

// 元素的 fullscreen 使用到了 screenfull 第三方库
const useFullscreen = (
  ref: RefObject<Element>,
  enabled: boolean,
  options: FullScreenOptions = {}
): boolean => {
  const { video, onClose = noop } = options;
  const [isFullscreen, setIsFullscreen] = useState(enabled);



  useIsomorphicLayoutEffect(() => {
    if (!enabled) {
      return;
    }
    if (!ref.current) {
      return;


    }




    const onWebkitEndFullscreen = () => {
      if (video?.current) {
        off(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
      }
      onClose();
    };

    const onChange = () => {
      if (screenfull.isEnabled) {
        const isScreenfullFullscreen = screenfull.isFullscreen;
        setIsFullscreen(isScreenfullFullscreen);
        if (!isScreenfullFullscreen) {
          onClose();
        }
      }

    };


    if (screenfull.isEnabled) {
      // 支持元素全屏
      try {
        screenfull.request(ref.current);
        setIsFullscreen(true);
      } catch (error) {
        onClose(error);
        setIsFullscreen(false);
      }
      screenfull.on('change', onChange);
    } else if (video && video.current && video.current.webkitEnterFullscreen) {
      // 不支持元素全屏,且 options 传递了 video 元素
      video.current.webkitEnterFullscreen();
      on(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
      setIsFullscreen(true);
    } else {
      onClose();
      setIsFullscreen(false);
    }

    return () => {
      // 卸载时退出全屏
      setIsFullscreen(false);
      if (screenfull.isEnabled) {
        try {
          screenfull.off('change', onChange);
          screenfull.exit();
        } catch {}
      } else if (video && video.current && video.current.webkitExitFullscreen) {
        off(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
        video.current.webkitExitFullscreen();
      }
    };
  }, [enabled, video, ref]);

  return isFullscreen;
};

screenfull Docs

Lifecycle

  • useMountedState()

使用说明:用来判断 hook 当前是否属于挂载状态

export default function useMountedState(): () => boolean {
  const mountedRef = useRef<boolean>(false);
  const get = useCallback(() => mountedRef.current, []);
  // 只在挂载时执行一次
  useEffect(() => {
    mountedRef.current = true;




    return () => {
      mountedRef.current = false;
    };

  }, []);




  return get;
}

  • useEffectOnce(effect)

使用说明:如下注释

const useEffectOnce = (effect: EffectCallback) => {
  // useEffect 第二个参数为空数组,因此 effect 副作用只会调用一次
  useEffect(effect, []);
};
  • useUpdateEffect(effect, deps)

使用说明:传入副作用、依赖,副作用只在二次挂载更新时执行

const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isFirstMount = useFirstMountState();




  useEffect(() => {

    if (!isFirstMount) {
      return effect();
    }
  }, deps);
};
  • useUnmount(fn)

使用说明:传入的 fn 只在 hook 卸载时执行一次

const useUnmount = (fn: () => any): void => {
  const fnRef = useRef(fn);




  // update the ref each render
  // so if it change the newest callback will be invoked
  fnRef.current = fn;




  // !!!关键点在这一步, 相当于 useEffect 销毁时的一次调用
  // useEffect(() => {
  //   return () => fnRef.current()
  // }, [])
  useEffectOnce(() => () => fnRef.current());
};
  • useLifecycles(mount[, unmount])

使用说明:传入两个函数,前者在挂载时执行,后者在卸载时执行

const useLifecycles = (mount, unmount?) => {
  useEffect(() => {

    if (mount) {
      mount();
    }

    return () => {
      if (unmount) {
        unmount();
      }
    };

  }, []);

};
  • useLogger
const useLogger = (componentName: string, ...rest) => {
  useEffectOnce(() => {
    console.log(`${componentName} mounted`, ...rest);
    return () => console.log(`${componentName} unmounted`);
  });




  useUpdateEffect(() => {
    console.log(`${componentName} updated`, ...rest);
  });
};

Animation

  • useUpdate()

使用说明:这个 hook 非常有意思,hook 调用返回一个函数,该函数执行则触发组件 re-render

const updateReducer = (num: number): number => (num + 1) % 1_000_000;




export default function useUpdate(): () => void {
  // NOTE: 利用了 useReducer => [state, dispatch] 特性, state 的更新隐藏在 dipatch 函数不向外暴露
  const [, update] = useReducer(updateReducer, 0);
  return update;
}
  • useInterval(callback, delay)
const useInterval = (callback: Function, delay?: number | null) => {
  const savedCallback = useRef<Function>(() => {});




  useEffect(() => {

    savedCallback.current = callback;
  });




  useEffect(() => {
    if (delay !== null) {
      const interval = setInterval(() => savedCallback.current(), delay || 0);
      return () => clearInterval(interval);
    }



    return undefined;
  }, [delay]);
};
  • useTimeout(ms = 0)

使用说明useTimeout 的源码很简单,内部调用的其实是 useUpdateuseTimeoutFn 这两个 hook;场景可用于设定 ms 时间后 re-render 组件

// useTimeout 返回值是一个包含三个函数的数组,[isReady, cancel, reset]
// isReady 函数调用会有3种返回值
//   - true: timeout执行了 false: timeout 未执行 null: timeout 被取消了



export type UseTimeoutReturn = [() => boolean | null, () => void, () => void];




export default function useTimeout(ms: number = 0): UseTimeoutReturn {
  // 该方法调用会触发组件 re-render
  const update = useUpdate();
  // !实际上是调用 useTimeoutFn
  return useTimeoutFn(update, ms);
}
  • useRaf(ms = 1e12, delay = 0)

使用说明:在 ms 时间内强制组件在每个 requestAnimationFrame 上重新渲染,返回经过的时间百分比。

const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;




const useRaf = (ms: number = 1e12, delay: number = 0): number => {
  const [elapsed, set] = useState<number>(0);


  useIsomorphicLayoutEffect(() => {
    let raf;
    let timerStop;
    let start;



    const onFrame = () => {
      const time = Math.min(1, (Date.now() - start) / ms);
      set(time);
      loop();
    };

    const loop = () => {
      raf = requestAnimationFrame(onFrame);
    };




    const onStart = () => {
      timerStop = setTimeout(() => {
        cancelAnimationFrame(raf);
        set(1);
      }, ms);

      start = Date.now();
      loop();
    };


    const timerDelay = setTimeout(onStart, delay);
    
    return () => {
      clearTimeout(timerStop);
      clearTimeout(timerDelay);
      cancelAnimationFrame(raf);
    };
  }, [ms, delay]);

  return elapsed;
};

总结

通过对 react-use 的源码的分析,我深刻感受到了 react hooks 对 react 原有开发模式(ClassComponent/HOC…)的颠覆以及其魅力,hook 的逻辑抽象组合复用能力杠杠的,且开发范式清晰明了,例如 react-use 很多 hooks 就是由一些简单的 hook 组合加成而来的(useUpdateEffect,useTimeout,useUpdate…),基础的 hook 遵循单一责任原则,可互相灵活配合成更强大的 hook;另外,阅读 react-use 源码也对 react hook 的执行机制更加了解、深刻。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYKU41gK' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片