前言
在react开发中离不开各种hooks.熟悉常见的hooks工具库有助于提升开发效率,在应对一些需求也能封装一些可用的hooks,这次我们来学习下react-use这个库。
react-use 文档 是用 storybook 搭建的。
如果公司项目需要搭建组件库或者 hooks、工具库等,storybook 或许是不错的选择。
目录:
分为六个章节:
-
Sensors
-
State
-
Side-effects
-
UI
-
LifeCycles
-
Animations
步骤:
git clone https://github.com/streamich/react-use.git
cd react-use
yarn install
yarn start
克隆项目到本地,安装依赖完成后,执行 yarn start。
Sensors行为:
useIdle
tracks whether user is being inactive. 跟踪用户是否处于非活动状态。
主要是:监听用户行为的事件(默认的 'mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel' ),指定时间内没有用户操作行为就是非活动状态。
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;
// 这里为什么需要localState???
let localState: boolean = state;
const set = (newState: boolean) => {
if (mounted) {
localState = newState;
setState(newState);
}
};
// 每次事件触发onEvent则会重新计时
const onEvent = throttle(50, () => {
if (localState) {
set(false);
}
clearTimeout(timeout);
timeout = setTimeout(() => set(true), ms);
});
const onVisibility = () => {
// 只有在标签页展示时才会计时间
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 () => {
// 将状态设置为unmounted
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;
useLocation:
useLocation docs | useLocation demo
React sensor hook that tracks brower's location.主要获取
window.location等对象信息。
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;
State状态
useFirstMountState
useFirstMountState docs | useFirstMountState demo
Returns true if component is just mounted (on first render) and false otherwise. 若组件刚刚加载(在第一次渲染时),则返回
true,否则返回false。
import { useRef } from 'react';
export function useFirstMountState(): boolean {
// useRef在重新渲染值不会更改
const isFirst = useRef(true);
if (isFirst.current) {
isFirst.current = false;
return true;
}
return isFirst.current;
}
usePrevious
usePrevious docs | usePrevious demo
React state hook that returns the previous state as described in the React hooks FAQ. 保留上一次的状态。
利用 useRef 的不变性。
import { useEffect, useRef } from 'react';
export default function usePrevious<T>(state: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
// ref改变并不会触发页面render,即函数不会重新运行,此时虽然值更新了,但是返回的值还是先前的值
ref.current = state;
});
return ref.current;
}
useSet
React state hook that tracks a Set.
new Set 的 hooks 用法。 useSet 可以用来列表展开、收起等其他场景。 返回 [set ,{add, remove, toggle, reset, has }]
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; // 判断是否存在某元素
}
// 接收Set,返回set集合和操作方法
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
useToggle docs | useToggle demo
tracks state of a boolean. 跟踪布尔值的状态。 切换 false => true => false
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;
Side-effect
useAsyncFn
useAsyncFn docs | 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 函数 fn,然后执行函数 fn.then()。 返回 state、callback(fn.then)。
/**
*
* @param fn 接收的函数或者异步函数
* @param deps 返回函数的依赖,依赖改变,返回函数会重新生成
* @param initialState 初始state
* @returns [state, callback as unknown as T]
*/
export default function useAsyncFn<T extends FunctionReturningPromise>(
fn: T,
deps: DependencyList = [],
initialState: StateFromFunctionReturningPromise<T> = { loading: false }
): AsyncFnReturn<T> {
const lastCallId = useRef(0);
// useMountedState: 判断组件是否加载
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
React hook that resolves an async function or a function that returns a promise; 解析异步函数或返回
promise的函数的React钩子;
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
useAsyncRetry docs | useAsyncRetry demo
Uses useAsync with an additional retry method to easily retry/refresh the async function; 使用useAsync额外·1返回支持重试/刷新的函数
主要就是变更依赖,次数(attempt),变更时会执行 useAsync 的 fn 函数。
const useAsyncRetry = <T>(fn: () => Promise<T>, deps: DependencyList = []) => {
const [attempt, setAttempt] = useState<number>(0);
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;
}
setAttempt((currentAttempt) => currentAttempt + 1);
}, [...deps, stateLoading]);
return { ...state, retry };
};
useDebounce
防抖:就是指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
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.
防抖
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);
// 取消上一次执行的函数
useEffect(reset, deps);
return [isReady, cancel];
}
useThrottle
节流:连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率
useThrottle docs | useThrottle demo
React hooks that throttle. 节流
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;
useEffect(() => {
if (!timeout.current) {
setState(value);
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 {
// 如果当前还在运行定时器,则记录新的value,写一次赋值给state
nextValue.current = value;
// 存在新值
hasNextValue.current = true;
}
}, [value]);
useUnmount(() => {
// 清除定时器
timeout.current && clearTimeout(timeout.current);
});
return state;
};
UI 用户界面
useFullscreen
useFullscreen docs | useFullscreen demo
Display an element full-screen, optional fallback for fullscreen video on iOS. 实现全屏
主要使用 screenfull npm 包实现.
const useFullscreen = (
ref: RefObject<Element>,
enabled: boolean,
options: FullScreenOptions = {}
): boolean => {
const { video, onClose = noop } = options;
const [isFullscreen, setIsFullscreen] = useState(enabled);
// const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
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 {
// .request(element, options?) Make an element fullscreen. Accepts a DOM element and FullscreenOptions.The default element is <html>.
// If called with another element than the currently active, it will switch to that if it's a descendant.
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;
};
LiCycles生命周期
useLifecycles
useLifecycles docs | useLifecycles demo
React lifecycle hook that call mount and unmount callbacks, when component is mounted and un-mounted, respectively.
React 生命周期挂钩,分别在组件安装和卸载时调用。
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钩子,它接受一个比较器,该比较器用于对依赖项进行比较,而不是对引用相等进行比较。
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);
if (!ref.current || !depsEqual(deps, ref.current)) {
ref.current = deps;
}
useEffect(effect, ref.current);
};
export default useCustomCompareEffect;
useDeepCompareEffect
useDeepCompareEffect docs | useDeepCompareEffect demo
A modified useEffect hook that is using deep comparison on its dependencies instead of reference equality. 一个修改后的
useEffect钩子,它对其依赖项使用深度比较,而不是引用相等
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
useUpdate docs | useUpdate demo
React utility hook that returns a function that forces component to re-render when called. React 实用程序钩子返回一个函数,该函数在调用时强制组件重新渲染。
主要用了 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;
}
总结
虽然没有全部hooks看完,但是通过分析若干react-use的自定义React hooks,也是有了很大的收获。