详解react-use,学完你也会写hook

3,448 阅读15分钟

react-use

文档:streamich.github.io/react-use/?…

川:juejin.cn/post/721635…

官方源码:github.com/streamich/r…

  • 举个例子

useEffectOnce

渲染时执行一次,相当于vuemounted

// react-use/src/useEffectOnce.ts
import { EffectCallback, useEffect } from 'react';

// 源码非常简单,不依赖任何参数的函数。

const useEffectOnce = (effect: EffectCallback) => {
  useEffect(effect, []);
};

export default useEffectOnce;
  • 执行源码中的测试用例 装了 jestjest runner vscode 插件,装完后测试用例中会直接显示 run、和 debug 按钮。 或者 终端 执行 npm -- -t "useFirstMountState或其他"

Sensors

useIdle

跟踪用户是否处于非活动状态。

监听用户行为的事件(默认的 'mousemove'【鼠标移动】, 'mousedown'【鼠标点击】, 'resize'【缩放】, 'keydown'【键盘】, 'touchstart'【移动端点击】, 'wheel'【滑轮滚动】 ),指定时间内没有用户操作行为就是非活动状态。

用法:

const isIdle = useIdle(3e3);
是否闲置了3s

原理和源码:

使用 usestate控制导出, 在useEffect中执行,确保只在渲染时只执行一次,设置定时器规定时间后设置statetrue。上述事件都绑定到window监听,且绑定同一个回调函数:使用了节流,当触发的时候如果当前statetrue,改为false,再设置定时器让其在规定时间后设置statetrue`

附加:

不仅支持上述事件,还支持了页面的显隐,当页面从隐藏切换到显示时也表示正在活动。

支持传入三个参数,时间,默认返回值,触发事件。当时间和事件变化时,重渲染。

mounted的目的:当时间和事件变化时,触发retrun销毁,原作用域的mounted设为false,期间定时器触发时,不再更改state。

localState的目的:替换state,不希望触发useEffect再执行。在onEvent触发时,需要获取当前state值从而判断是否需要修改,若有时间和事件变化,为了获取state新值,需要将其也加入effect依赖项中,这样加入会导致,当state更改时要重新触发effect执行。为了避免,所以加入localState来平替。

import { useEffect, useState } from 'react';
// 防抖、节流
import { throttle } from 'throttle-debounce';
// 事件解绑和监听函数
import { off, on } from './misc/util';

// 监听默认事件
const defaultEvents = ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel'];
const oneMinute = 60e3;

const useIdle = (
  ms: number = oneMinute,
  initialState: boolean = false,
  events: string[] = defaultEvents
): boolean => {
  const [state, setState] = useState<boolean>(initialState);

  useEffect(() => {
    let mounted = true;
    let timeout: any;
    let localState: boolean = state;
    const set = (newState: boolean) => {
      if (mounted) {
        localState = newState;
        setState(newState);
      }
    };

    const onEvent = throttle(50, () => {
      if (localState) {
        set(false);
      }

      clearTimeout(timeout);
      timeout = setTimeout(() => set(true), ms);
    });
    const onVisibility = () => {
      // 如果页面活动中
      // hidden 页面内容不可见。意味着该页面是后台标签页或者最小化,或者系统是锁屏状态等
      if (!document.hidden) {
        onEvent();
      }
    };

    for (let i = 0; i < events.length; i++) {
      on(window, events[i], onEvent);
    }
    on(document, 'visibilitychange', onVisibility);

    timeout = setTimeout(() => set(true), ms);

    return () => {
      mounted = false;

      // 销毁 解绑事件
      for (let i = 0; i < events.length; i++) {
        off(window, events[i], onEvent);
      }
      off(document, 'visibilitychange', onVisibility);
    };
  }, [ms, events]);

  return state;
};

export default useIdle;
  • 扩展 document.hiddenvisibilitychange

https://blog.csdn.net/weixin_42884485/article/details/96477581
其中document.hidden的值是一个布尔值,表示标签页的显示或隐藏。

这个hook用visibilitychange来监听变化,用document.hidden来判断状态

而document.visibilityState相对详细一些,目前有四个可能的值:
visibble:页面部分内容可见。意味着该标签页是一个非最小化的可见标签页,可能被别的页面覆盖了一部分。
hidden:页面内容不可见。意味着该页面是后台标签页或者最小化,或者系统是锁屏状态等。
prerender:网页内容被预渲染并且用户不可见。
unloaded:如果文档被卸载,将返回这个值。
应用场景例如:标签页隐藏的时候暂停播放流媒体文件、停止一些不必要的轮询;页面显示的时候出现提示弹窗(点击支付跳转到新开页面,再返回这个页面时弹出支付状态相关提示弹窗)等等。

useLocation

获取historylocation信息。

用法:

const state = useLocation();
state:{
  "trigger": "pushstate",
  "state": {},
  "length": 19,
  "hash": "",
  "host": "streamich.github.io",
  "hostname": "streamich.github.io",
  "href": "https://streamich.github.io/react-use/page-2",
  "origin": "https://streamich.github.io",
  "pathname": "/react-use/page-2",
  "port": "",
  "protocol": "https:",
  "search": ""
}

原理:

获取historylocation信息。将historylocation合并返回,当路由跳转时,重新获取数据。

附加:

还有trigger信息,表示当前页面是通过loadpopStatepushStatereplaceState进入的,这个需要做事件监听。

其中popstate原生支持监听,但另外两个不行,所以需要加事件,操作和vue2的数组依赖监听类似,更改原pushStatereplaceState方法,在执行原操作的基础上,添加新的自定义事件并触发。

当环境不是浏览器时,就只是简单的返回,这里判断是否是浏览器不仅用了isBrowser,还有判断event是否是函数

其中 isBrowser = typeof window !== 'undefined'; 简单说就是判断有无windowevent

import { useEffect, useState } from 'react';
// 判断浏览器
import { isBrowser, off, on } from './misc/util';

const patchHistoryMethod = (method) => {
  const history = window.history;
  const original = history[method];

  history[method] = function (state) {
    // 原先函数
    const result = original.apply(this, arguments);
    // 自定义事件 new Event 、 dispatchEvent
    const event = new Event(method.toLowerCase());

    (event as any).state = state;

    window.dispatchEvent(event);

    return result;
  };
};

if (isBrowser) {
  patchHistoryMethod('pushState');
  patchHistoryMethod('replaceState');
}
// 省略 LocationSensorState 类型

const useLocationServer = (): LocationSensorState => ({
  trigger: 'load',
  length: 1,
});

const buildState = (trigger: string) => {
  const { state, length } = window.history;

  const { hash, host, hostname, href, origin, pathname, port, protocol, search } = window.location;

  return {
    trigger,
    state,
    length,
    hash,
    host,
    hostname,
    href,
    origin,
    pathname,
    port,
    protocol,
    search,
  };
};

const useLocationBrowser = (): LocationSensorState => {
  const [state, setState] = useState(buildState('load'));

  useEffect(() => {
    const onPopstate = () => setState(buildState('popstate'));
    const onPushstate = () => setState(buildState('pushstate'));
    const onReplacestate = () => setState(buildState('replacestate'));

    on(window, 'popstate', onPopstate);
    on(window, 'pushstate', onPushstate);
    on(window, 'replacestate', onReplacestate);

    return () => {
      off(window, 'popstate', onPopstate);
      off(window, 'pushstate', onPushstate);
      off(window, 'replacestate', onReplacestate);
    };
  }, []);

  return state;
};

const hasEventConstructor = typeof Event === 'function';

export default isBrowser && hasEventConstructor ? useLocationBrowser : useLocationServer;
  • 扩展:axios中判断时node或者浏览器 判断有 XMLHttpRequestprocess
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }

作者:_骁
链接:https://juejin.cn/post/7039548213480652813
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

State

useFirstMountState

若组件刚刚加载(在第一次渲染时),则返回 true,否则返回 false

用法: demo

 const isFirstMount = useFirstMountState();

原理:

利用useRef不变的特性,默认为true,判断如果为true将其设置为false并返回true,不为true则返回useRef 【应该也可以直接else return false吧】,代码短小精湛。

import { useRef } from 'react';

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

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

    return true;
  }

  return isFirst.current;
}
为了校验自己的想法,我自己改了源码
改成
  if (isFirst.current) {
    isFirst.current = false;
    return true;
  }else{
  	return false
  }
进行单元测试,npm  -- -t "useFirstMountState"
 PASS  tests/useFirstMountState.test.ts
Test Suites: 75 skipped, 1 passed, 1 of 76 total
Tests:       485 skipped, 3 passed, 488 total
Snapshots:   0 total
Time:        6.819 s
通过了!!!其他单测方法:https://www.likecs.com/ask-76919.html

usePrevious

和上面的类似,利用useRef不变的特性,还有effect异步执行特性,延迟保存新值,先返回旧值。

保留上一次的状态。

用法:demo 初始是undefined,重渲染后显示传入值之前的值。

import {usePrevious} from 'react-use';

const Demo = () => {
  const [count, setCount] = React.useState(0);
  const prevCount = usePrevious(count);

  return (
    <p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <p>
        Now: {count}, before: {prevCount}
      </p>
    </p>
  );
};

源码:

import { useEffect, useRef } from 'react';

export default function usePrevious<T>(state: T): T | undefined {
  const ref = useRef<T>();

  useEffect(() => {
    ref.current = state;
  });

  return ref.current;
}

useSet

new Set 的 hooks 用法。 useSet 可以用来列表展开、收起等其他场景。 返回 [set ,{add, remove, toggle, reset, has }]

 const [set, { add, has, remove, toggle, reset }] = useSet(new Set(['hello']));

demo

原理:用useState返回,逻辑基本都是转成数组来处理再转为set

import { useCallback, useMemo, useState } from 'react';

export interface StableActions<K> {
  add: (key: K) => void;
  remove: (key: K) => void;
  toggle: (key: K) => void;
  reset: () => void;
}

export interface Actions<K> extends StableActions<K> {
  has: (key: K) => boolean;
}

const useSet = <K>(initialSet = new Set<K>()): [Set<K>, Actions<K>] => {
  const [set, setSet] = useState(initialSet);

  const stableActions = useMemo<StableActions<K>>(() => {
    const add = (item: K) => setSet((prevSet) => new Set([...Array.from(prevSet), item]));
    const remove = (item: K) =>
      setSet((prevSet) => new Set(Array.from(prevSet).filter((i) => i !== item)));
    const toggle = (item: K) =>
      setSet((prevSet) =>
        prevSet.has(item)
          ? new Set(Array.from(prevSet).filter((i) => i !== item))
          : new Set([...Array.from(prevSet), item])
      );

    return { add, remove, toggle, reset: () => setSet(initialSet) };
  }, [setSet]);

  const utils = {
    has: useCallback((item) => set.has(item), [set]),
    ...stableActions,
  } as Actions<K>;

  return [set, utils];
};

export default useSet;

useToggle

tracks state of a boolean. 跟踪布尔值的状态。 切换 false => true => false

demo

用法:

const Demo = () => {
  const [on, toggle] = useToggle(true);

  return (
    <div>
      <div>{on ? 'ON' : 'OFF'}</div>
      <button onClick={toggle}>Toggle</button>
      <button onClick={() => toggle(true)}>set ON</button>
      <button onClick={() => toggle(false)}>set OFF</button>
    </div>
  );
};

源码:

import { Reducer, useReducer } from 'react';

const toggleReducer = (state: boolean, nextValue?: any) =>
  typeof nextValue === 'boolean' ? nextValue : !state;

const useToggle = (initialValue: boolean): [boolean, (nextValue?: any) => void] => {
  return useReducer<Reducer<boolean, any>>(toggleReducer, initialValue);
};

export default useToggle;

和我想的用 useState 不一样,他用了 Reducer

这里讲一下 Reducer useReducer 的例子

useReducer 接收 处理state的函数reducer 和 初始值,返回state和触发reducer的方法。在reducer的返回值直接赋值给state

一般reducer都是这种switch的形状,而源码这里只用了简单的判断。

扩展:useRucer的实现,有点多,下次。。 自己用useState做的,过了单测,嘿嘿

image-20230518205625902.png

lifestyle

useMountedState

demo

组件是否已挂载

该钩子不会导致组件重新呈现。此组件设计用于避免对未安装的组件进行状态更新。

Lifecycle hook providing ability to check component's mount state. Returns a function that will return true if component mounted and false otherwise. 生命周期挂钩提供了检查组件装载状态的能力。 返回一个函数,如果组件已安装,则返回true,否则返回false。

原理:有点抽象,定义一个ref,在effect异步的时候改为trueeffect执行说明挂载了,当你使用的时候返回ref,如果为true就说明挂载了

import { useCallback, useEffect, useRef } from 'react';

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;
}

side-effects

useAsyncFn

demo

React hook that returns state and a callback for an async function or a function that returns a promise. The state is of the same shape as useAsync. 为异步函数或返回promise的函数返回状态和回调的React钩子。状态与useAsync的形状相同。

用法:很容易看出就是执行传入的promise函数,返回state和再次执行的方法。其中state提供了loading属性来判断是否正在执行函数

import {useAsyncFn} from 'react-use';

const Demo = ({url}) => {
  const [state, doFetch] = useAsyncFn(async () => {
    const response = await fetch(url);
    const result = await response.text();
    return result
  }, [url]);

  return (
    <div>
      {state.loading
        ? <div>Loading...</div>
        : state.error
          ? <div>Error: {state.error.message}</div>
          : <div>Value: {state.value}</div>
      }
      <button onClick={() => doFetch()}>Start loading</button>
    </div>
  );
};

源码: 和 useMountedState 有关,useAsyncFn 主要是传入promise函数,然后执行它,返回 state 和执行

// 省略若干代码
export default function useAsyncFn<T extends FunctionReturningPromise>(
// 接收三个参数 promise函数,依赖,初始值
  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> => {
    // 这个id主要用来判断当前的请求和之前的请求,学到了
    const callId = ++lastCallId.current;
	// 执行请求前 先改loading为true
    if (!state.loading) {
      set((prevState) => ({ ...prevState, loading: true }));
    }
	// 执行请求
    return fn(...args).then(
      (value) => {
        // 是否挂载 是否同请求 是就把loading改为false 并返回值
        // 为什么说 callId === lastCallId.current 可以辨别是否同请求 当url更改时 之前的请求 id 和 lastid 为1;更改后,id和lastid为2,之前请求完成后,就会出现这里id为1,lastid为2的情况,不会去重置loading。
        // 这里的return value 只在 doFetch.then里获取
        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

demo

React hook that resolves an async function or a function that returns a promise; 解析异步函数或返回 promise 的函数的 React 钩子;

原理:就是会自动执行一次useAsyncFn

import { DependencyList, useEffect } from 'react';
import useAsyncFn from './useAsyncFn';
import { FunctionReturningPromise } from './misc/types';

export { AsyncState, AsyncFnReturn } from './useAsyncFn';

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

demo

Uses useAsync with an additional retry method to easily retry/refresh the async function; 在 useAsync 上加个retry的方法。

变更依赖,次数(attempt),变更时会执行 useAsyncfn 函数。

个人觉得还不如直接用useAsyncFn里的dofetch

源码:

import { DependencyList, useCallback, useState } from 'react';
import useAsync, { AsyncState } from './useAsync';

export type AsyncStateRetry<T> = AsyncState<T> & {
  retry(): void;
};

const useAsyncRetry = <T>(fn: () => Promise<T>, deps: DependencyList = []) => {
  // 一个记录次数的变量
  const [attempt, setAttempt] = useState<number>(0);
  // 次数加入依赖
  const state = useAsync(fn, [...deps, attempt]);

  const stateLoading = state.loading;
  // retry时改次数 useAsync依赖感知变动 改了callback 改了callback变动触发useEffect再次执行~
  const retry = useCallback(() => {
    // 省略开发环境警告提示

    setAttempt((currentAttempt) => currentAttempt + 1);
  }, [...deps, stateLoading]);

  return { ...state, retry };
};

export default useAsyncRetry;

useTimeoutFn(Animations)

useTimeoutFn 属于 Animations 模块,但这个 hookuseDebounce 中使用,所以放到这里讲述。

demo

Calls given function after specified amount of milliseconds. 在指定的毫秒数后调用给定的函数。

其实简单的防抖 应该就是把按钮的点击改成 reset 去执行

个人用法:

  const [isReady, cancel, reset] = useTimeoutFn(fn, 5000);
  <button onclick='reset'> 点击 </button>

官方:

const Demo = () => {
  const [state, setState] = React.useState('Not called yet');

  function fn() {
    setState(`called at ${Date.now()}`);
  }
	// isReady()能获取当前state,
  // cancel 取消定时器,把state改为null
	// reset 重执行 state改为false,重启定时任务,执行完后state为true
  // 5s后执行fn 触发重渲染 ready就可以获取到新值
  const [isReady, cancel, reset] = useTimeoutFn(fn, 5000);
  const cancelButtonClick = useCallback(() => {
    if (isReady() === false) {
      cancel();
      setState(`cancelled`);
    } else {
      reset();
      setState('Not called yet');
    }
  }, []);
	// 获取到 ready值 
  const readyState = isReady();

  return (
    <div>
     // 这里ready默认为false,取消后为null
      <div>{readyState !== null ? 'Function will be called in 5 seconds' : 'Timer cancelled'}</div>
    // ready默认为false,所以为 cancel restart 
    // 点击执行 初始ready为false  执行cancel 执行完后 ready 变成 true ,变成restart  执行setState(`cancelled`);
    // restart执行后 重开定时器
      <button onClick={cancelButtonClick}> {readyState === false ? 'cancel' : 'restart'} timeout</button>
      <br />
    // 默认为pending 、 执行完成后为called,取消为null 为cancelled 这里的三种情况很有意思 false、true 和 null
      <div>Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled'}</div>
      <div>{state}</div>
    </div>
  );
};

原理:

主要是 useRefsetTimeout 结合实现的,防抖的思想。

源码:

import { useCallback, useEffect, useRef } from 'react';

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

export default function useTimeoutFn(fn: Function, ms: number = 0): UseTimeoutFnReturn {
  const ready = useRef<boolean | null>(false);
  const timeout = useRef<ReturnType<typeof setTimeout>>();
  const callback = useRef(fn);
// isReady()可以获取当前执行状况 这里值得学习 用 ()=>ref.current 获取最新值 而不需要依赖 不会重渲染
  const isReady = useCallback(() => ready.current, []);

  const set = useCallback(() => {
    // ready改为false 清除定时器
    ready.current = false;
    timeout.current && clearTimeout(timeout.current);
 // 重启定时器 这里和防抖一样
    timeout.current = setTimeout(() => {
      ready.current = true;
      callback.current();
    }, ms);
  }, [ms]);

  const clear = useCallback(() => {
    // 清除时 ready为null
    ready.current = null;
    timeout.current && clearTimeout(timeout.current);
  }, []);

  // update ref when function changes
  useEffect(() => {
    // 当fn改时,更新callback
    // 初始化
    callback.current = fn;
  }, [fn]);

  // set on mount, clear on unmount
  useEffect(() => {
    set();
		// 当时间更改时 重跑 
    // 组件卸载时 去掉定时器
    // 默认执行一次
    return clear;
  }, [ms]);

  return [isReady, clear, set];
}

useDebounce

useDebounce docs | useDebounce demo

React hook that delays invoking a function until after wait milliseconds have elapsed since the last time the debounced function was invoked. 防抖

用法:

// 输入后 2s后更新debouncedValue 2s期间更改输入 打断施法 重启定时器
const Demo = () => {
  const [state, setState] = React.useState('Typing stopped');
  const [val, setVal] = React.useState('');
  const [debouncedValue, setDebouncedValue] = React.useState('');
// 需要加依赖项 通过依赖项变动 带动防抖执行
  const [, cancel] = useDebounce(
    () => {
      setState('Typing stopped');
      setDebouncedValue(val);
    },
    2000,
    [val]
  );

  return (
    <div>
      <input
        type="text"
        value={val}
        placeholder="Debounced input"
        onChange={({ currentTarget }) => {
          setState('Waiting for typing to stop...');
          setVal(currentTarget.value);
        }}
      />
      <div>{state}</div>
      <div>
        Debounced value: {debouncedValue}
        <button onClick={cancel}>Cancel debounce</button>
      </div>
    </div>
  );
};

原理:配合useTimeoutFn,传入依赖,依赖变动重新执行 resst

源码:

import { DependencyList, useEffect } from 'react';
import useTimeoutFn from './useTimeoutFn';

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

export default function useDebounce(
  fn: Function,
  ms: number = 0,
  deps: DependencyList = []
): UseDebounceReturn {
  const [isReady, cancel, reset] = useTimeoutFn(fn, ms);
	// 默认执行 fn , 依赖变动时 触发防抖
  useEffect(reset, deps);
	// 返回状态 和 取消函数
  return [isReady, cancel];
}

useThrottle

demo

和一般的节流不太一样,一般节流是频繁触发时,一个时间段里只能触发一次;这个还有点防抖的感觉,频繁改值时,先是只取第一个值,一定时间后,自动赋值最新值,而一般的节流需要我们主动触发。

原理:传入value和时间戳,value频繁更新,节流去重渲染value。 第一次传入value时 立刻更新为state,开定时器(一定时间后检查是否有新值,有则更新并重开定时器),后面紧跟着的不同value传入时,将值记录为新值。

源码:

import { useEffect, useRef, useState } from 'react';
import useUnmount from './useUnmount';

const useThrottle = <T>(value: T, ms: number = 200) => {
  // 状态
  const [state, setState] = useState<T>(value);
  // 定时器
  const timeout = useRef<ReturnType<typeof setTimeout>>();
  // 新值
  const nextValue = useRef(null) as any;
  //  是否有新值                        
  const hasNextValue = useRef(0) as any;
	// value值变化时触发
  useEffect(() => {
    // 如果没有定时器 一开始肯定没有的 更新state 定时器
    if (!timeout.current) {
      // 立刻更新 value
      setState(value);
      // 有新值,新增标识取消,更新新值到state,重新计时
      // 没有新值 timeout标识关闭
      const timeoutCallback = () => {
        if (hasNextValue.current) {
          hasNextValue.current = false;
          setState(nextValue.current);
          timeout.current = setTimeout(timeoutCallback, ms);
        } else {
          timeout.current = undefined;
        }
      };
      // 定时器
      timeout.current = setTimeout(timeoutCallback, ms);
      
    } else {
      // 有定时器
      // 赋值新值
      // 有新值标识
      nextValue.current = value;
      hasNextValue.current = true;
    }
  }, [value]);
 // 离开的时候销毁定时器
  useUnmount(() => {
    timeout.current && clearTimeout(timeout.current);
  });

  return state;
};

export default useThrottle;

UI

useFullscreen

demo

Display an element full-screen, optional fallback for fullscreen video on iOS. 实现全屏

用法:

import {useFullscreen, useToggle} from 'react-use';

const Demo = () => {
  const ref = useRef(null)
  const [show, toggle] = useToggle(false);
  const isFullscreen = useFullscreen(ref, show, {onClose: () => toggle(false)});
  
  触发 toggle() 让show为true ref挂载的元素 变成 全屏

原理:主要使用 screenfull npm 包实现,只能用在能全屏的,如video,有元素自带的全屏方法 webkitEnterFullscreen,涉及到其他库,不继续研究了。

源码:

import { RefObject, useState } from 'react';
import screenfull from 'screenfull';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
import { noop, off, on } from './misc/util';

export interface FullScreenOptions {
  video?: RefObject<
    HTMLVideoElement & { webkitEnterFullscreen?: () => void; webkitExitFullscreen?: () => void }
  >;
  onClose?: (error?: Error) => void;
}

const useFullscreen = (
  ref: RefObject<Element>,
  enabled: boolean,
  options: FullScreenOptions = {}
): boolean => {
  const { video, onClose = noop } = options;
  const [isFullscreen, setIsFullscreen] = useState(enabled);
// useLayoutEffect that does not show warning when server-side rendering
  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) {
      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;
};

export default useFullscreen;

Lifecycles

useLifecycles

React lifecycle hook that call mount and unmount callbacks, when component is mounted and un-mounted, respectively. React 生命周期挂钩,分别在组件安装和卸载时调用。

用法:

const Demo = () => {
  useLifecycles(() => console.log('MOUNTED'), () => console.log('UNMOUNTED'));
  return null;
};

原理:利用 useEffect,传入空数组依赖,return只在组件卸载时执行

源码:

import { useEffect } from 'react';

const useLifecycles = (mount, unmount?) => {
  useEffect(() => {
    if (mount) {
      mount();
    }
    return () => {
      if (unmount) {
        unmount();
      }
    };
  }, []);
};

export default useLifecycles;

useCustomCompareEffect

useCustomCompareEffect docs | useCustomCompareEffect demo

A modified useEffect hook that accepts a comparator which is used for comparison on dependencies instead of reference equality. 一个经过修改的useEffect钩子,它接受一个比较器,该比较器用于对依赖项进行比较,而不是对引用相等进行比较。

用法:

  const [count, {inc: inc}] = useCounter(0);
  const options = { step: 2 };

  useCustomCompareEffect(() => {
    inc(options.step)
  }, [options], (prevDeps, nextDeps) => isEqual(prevDeps, nextDeps));
  这里 count 应该为 2,后面每次重渲染都会加2,因为依赖一直相等 符合条件 再次加2

原理:根据传入方法来判断决定传入useEffect的依赖是否变化

源码:

import { DependencyList, EffectCallback, useEffect, useRef } from 'react';

const isPrimitive = (val: any) => val !== Object(val);

type DepsEqualFnType<TDeps extends DependencyList> = (prevDeps: TDeps, nextDeps: TDeps) => boolean;

const useCustomCompareEffect = <TDeps extends DependencyList>(
  effect: EffectCallback,
  deps: TDeps,
  depsEqual: DepsEqualFnType<TDeps>
) => {
  // 省略一些开发环境的警告提示

  const ref = useRef<TDeps | undefined>(undefined);
	// 一开始没有ref.current 赋值
  // 传入的deps和之前的deps深比较,不一样则重新赋值
  if (!ref.current || !depsEqual(deps, ref.current)) {
    // 我本来有疑问 deps 有没有可能 引用不变 导致useEfFect认为无变化而不重新执行 但实际上如用法中的例子,每次重渲染变量的引用都变了,除非是ref等hook这类。
    // 是的,像上面例子的用法 用在useEffect上会导致每次重渲染都重新执行
    ref.current = deps;
  }
	// ref.current 重赋值, 触发重新执行 
  useEffect(effect, ref.current);
};

export default useCustomCompareEffect;

useDeepCompareEffect

A modified useEffect hook that is using deep comparison on its dependencies instead of reference equality. 一个修改后的 useEffect 钩子,它对其依赖项使用深度比较,而不是引用相等。

用法:

  const [count, {inc: inc}] = useCounter(0);
  const options = { step: 2 };

  useDeepCompareEffect(() => {
    inc(options.step)
  }, [options]);

原理: 就是 useCustomCompareEffect 的 判断条件固定为 深比较

源码:

import { DependencyList, EffectCallback } from 'react';
import useCustomCompareEffect from './useCustomCompareEffect';
import isDeepEqual from './misc/isDeepEqual';

const isPrimitive = (val: any) => val !== Object(val);

const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
  // 省略若干开发环境的警告提示

  useCustomCompareEffect(effect, deps, isDeepEqual);
};

export default useDeepCompareEffect;

Animations

useUpdate

介绍:强行重渲染

原理:主要用了 useReducer 每次调用 updateReducer 方法,来达到强制组件重新渲染的目的。

源码:

import { useReducer } from 'react';

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

export default function useUpdate(): () => void {
  const [, update] = useReducer(updateReducer, 0);

  return update;
}

我自己实现的: 不过没过单测。

export default function useUpdate() {
  const [, update] = useState(new Date());
  const fn = ()=>{
    update(new Date())
  }
  return fn;
}

若川题外话:

学习 ahooks别人写的 ahooks 源码分析beautiful-react-hooksmantine-hooks 等。

学习过程中带着问题多查阅 React 新文档 react.dev新中文文档 zh-hans.react.dev,相信收获更大。

如果技术栈是 Vue,感兴趣的小伙伴可以学习 VueUse

  • 若川文章里的就这么多,如果点赞多就继续更