React钩子封装记录

react-use

副作用钩子

useTitle

用于设置导航栏标题。

语法:

1
useTitle(title, options)

参数:

  • title:{String},标题文案
  • options:{Object},参数,可选
    • restoreOnUnmount:{Boolean}是否退出还原标题

使用:

1
2
3
4
5
6
7
import {useTitle} from 'react-use';

const Demo = () => {
useTitle('Hello title!');

return null;
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/useTitle.ts

import { useRef, useEffect } from 'react';
export interface UseTitleOptions {
restoreOnUnmount?: boolean;
}
const DEFAULT_USE_TITLE_OPTIONS: UseTitleOptions = {
restoreOnUnmount: false,
};
function useTitle(title: string, options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS) {
const prevTitleRef = useRef(document.title);
document.title = title;
useEffect(() => {
if (options && options.restoreOnUnmount) {
return () => {
document.title = prevTitleRef.current;
};
} else {
return;
}
}, []);
}

export default typeof document !== 'undefined' ? useTitle : (_title: string) => {};

其中主要是利用document.title来设置页面标题,还原标题则是借助ref来进行旧标题暂存。

useCookie

用于cookie的处理。

语法:

1
const [value, updateCookie, deleteCookie] = useTitle(cookieName)

参数:

  • cookieName{String},cookie标识

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useCookie } from "react-use";

const Demo = () => {
const [value, updateCookie, deleteCookie] = useCookie("my-cookie");
const [counter, setCounter] = useState(1);

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

const updateCookieHandler = () => {
updateCookie(`my-awesome-cookie-${counter}`);
setCounter(c => c + 1);
};

return (
<div>
<p>Value: {value}</p>
<button onClick={updateCookieHandler}>Update Cookie</button>
<br />
<button onClick={deleteCookie}>Delete Cookie</button>
</div>
);
};

源码及解析
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
// src/useCookie.ts


import { useState, useCallback } from 'react';
import Cookies from 'js-cookie';

const useCookie = (
cookieName: string
): [string | null, (newValue: string, options?: Cookies.CookieAttributes) => void, () => void] => {
const [value, setValue] = useState<string | null>(() => Cookies.get(cookieName) || null);

const updateCookie = useCallback(
(newValue: string, options?: Cookies.CookieAttributes) => {
Cookies.set(cookieName, newValue, options);
setValue(newValue);
},
[cookieName]
);

const deleteCookie = useCallback(() => {
Cookies.remove(cookieName);
setValue(null);
}, [cookieName]);

return [value, updateCookie, deleteCookie];
};

export default useCookie;
  • 1.核心的cookie操作是基于js-cookie这个封装库
  • 2.通过useState控制cookie的取值状态

useLocalStorage

用于localStorage的处理。

语法:

1
2
3
4
5
const [value, setValue, remove] = useLocalStorage(key, initialValue, {
raw: false,
serializer: (value: T) => string,
deserializer: (value: string) => T,
});

参数:

  • key:{String},storage标识
  • initialValue:{any},storage值
  • options:参数,可选
    • raw:{Boolean},是否自定义加工处理。默认false,为JSON.stringify/JSON.parse。
    • serializer:{Function},当options.rawtrue时生效,设置storage值的加工函数
    • deserializer:{Function},当options.rawtrue时生效,读取storage值的加工函数

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useLocalStorage } from 'react-use';

const Demo = () => {
const [value, setValue, remove] = useLocalStorage('my-key', 'foo');

return (
<div>
<div>Value: {value}</div>
<button onClick={() => setValue('bar')}>bar</button>
<button onClick={() => setValue('baz')}>baz</button>
<button onClick={() => remove()}>Remove</button>
</div>
);
};

源码及解析
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// src/useLocalStorage.ts

import { useState, useCallback, Dispatch, SetStateAction } from 'react';
import { isClient } from './util';

type parserOptions<T> =
| {
raw: true;
}
| {
raw: false;
serializer: (value: T) => string;
deserializer: (value: string) => T;
};

const noop = () => {};

const useLocalStorage = <T>(
key: string,
initialValue?: T,
options?: parserOptions<T>
): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => void] => {
if (!isClient) {
return [initialValue as T, noop, noop];
}
if (!key) {
throw new Error('useLocalStorage key may not be falsy');
}

const deserializer = options ? (options.raw ? value => value : options.deserializer) : JSON.parse;

const [state, setState] = useState<T | undefined>(() => {
try {
const serializer = options ? (options.raw ? String : options.serializer) : JSON.stringify;

const localStorageValue = localStorage.getItem(key);
if (localStorageValue !== null) {
return deserializer(localStorageValue);
} else {
initialValue && localStorage.setItem(key, serializer(initialValue));
return initialValue;
}
} catch {
// If user is in private mode or has storage restriction
// localStorage can throw. JSON.parse and JSON.stringify
// can throw, too.
return initialValue;
}
});

const set: Dispatch<SetStateAction<T | undefined>> = useCallback(
valOrFunc => {
try {
const newState = typeof valOrFunc === 'function' ? (valOrFunc as Function)(state) : valOrFunc;
if (typeof newState === 'undefined') return;
let value: string;

if (options)
if (options.raw)
if (typeof newState === 'string') value = newState;
else value = JSON.stringify(newState);
else if (options.serializer) value = options.serializer(newState);
else value = JSON.stringify(newState);
else value = JSON.stringify(newState);

localStorage.setItem(key, value);
setState(deserializer(value));
} catch {
// If user is in private mode or has storage restriction
// localStorage can throw. Also JSON.stringify can throw.
}
},
[key, setState]
);

const remove = useCallback(() => {
try {
localStorage.removeItem(key);
setState(undefined);
} catch {
// If user is in private mode or has storage restriction
// localStorage can throw.
}
}, [key, setState]);

return [state, set, remove];
};

export default useLocalStorage;
  • 1.对非浏览器环境(用window变量判断)做了处理,直接返回原值,操作函数为空函数;
    1. serializer/deserializer处理函数用于hook state调用,不影响localStorage的读写;
  • 3.参数raw为false时,默认设置storage操作时会执行JSON.stringify(val)处理,读取storage操作时会执行JSON.parse(val)处理;
  • 4.参数raw为true时,serializer默认为String,即设置storage操作时执行String(val)处理;deserializer默认为value => value,即读取storage操作时会直接返回;

useSessionStorage

用于sessionStorage的处理。

语法:

1
2
3
const [value, setValue] = useSessionStorage(key, initialValue, {
raw: false,
});

参数:

  • key:{String},storage标识
  • initialValue:{any},storage值
  • options:参数,可选
    • raw:{Boolean},是否自定义加工处理。默认false,为JSON.stringify/JSON.parse。

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
import {useSessionStorage} from 'react-use';

const Demo = () => {
const [value, setValue] = useSessionStorage('my-key', 'foo');

return (
<div>
<div>Value: {value}</div>
<button onClick={() => setValue('bar')}>bar</button>
<button onClick={() => setValue('baz')}>baz</button>
</div>
);
};

源码及解析
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
40
41
// src/useSessionStorage.ts

import { useEffect, useState } from 'react';
import { isClient } from './util';

const useSessionStorage = <T>(key: string, initialValue?: T, raw?: boolean): [T, (value: T) => void] => {
if (!isClient) {
return [initialValue as T, () => {}];
}

const [state, setState] = useState<T>(() => {
try {
const sessionStorageValue = sessionStorage.getItem(key);
if (typeof sessionStorageValue !== 'string') {
sessionStorage.setItem(key, raw ? String(initialValue) : JSON.stringify(initialValue));
return initialValue;
} else {
return raw ? sessionStorageValue : JSON.parse(sessionStorageValue || 'null');
}
} catch {
// If user is in private mode or has storage restriction
// sessionStorage can throw. JSON.parse and JSON.stringify
// cat throw, too.
return initialValue;
}
});

useEffect(() => {
try {
const serializedState = raw ? String(state) : JSON.stringify(state);
sessionStorage.setItem(key, serializedState);
} catch {
// If user is in private mode or has storage restriction
// sessionStorage can throw. Also JSON.stringify can throw.
}
});

return [state, setState];
};

export default useSessionStorage;
  • 1.对非浏览器环境(用window变量判断)做了处理,直接返回原值,操作函数为空函数;
  • 2.参数raw为false时,默认设置storage操作时会执行JSON.stringify(val)处理,读取storage操作时会执行JSON.parse(val)处理;
  • 3.sessionStorage的应用场景相对localStorage较小,因此无需发布订阅的控制

useRafLoop

控制requestAnimationFrame循环。

语法:

1
const [stopLoop, startLoop, isActive] = useRafLoop(callback: FrameRequestCallback, initiallyActive = true);

参数:

  • callback:{Function},RAF片段的控制回调函数
  • initiallyActive: {boolean},是否已开始就激活循环,默认为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 * as React from 'react';
import { useRafLoop, useUpdate } from 'react-use';

const Demo = () => {
const [ticks, setTicks] = React.useState(0);
const [lastCall, setLastCall] = React.useState(0);
const update = useUpdate();

const [loopStop, loopStart, isActive] = useRafLoop((time) => {
setTicks(ticks => ticks + 1);
setLastCall(time);
});

return (
<div>
<div>RAF triggered: {ticks} (times)</div>
<div>Last high res timestamp: {lastCall}</div>
<br />
<button onClick={() => {
isActive() ? loopStop() : loopStart();
update();
}}>{isActive() ? 'STOP' : 'START'}</button>
</div>
);
};

源码及解析
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
40
41
42
43
44
45
46
47
// src/useRafLoop.ts

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

export type RafLoopReturns = [() => void, () => void, () => boolean];

export default function useRafLoop(callback: FrameRequestCallback, initiallyActive = true): RafLoopReturns {
const raf = useRef<number | null>(null);
const rafActivity = useRef<boolean>(false);
const rafCallback = useRef(callback);
rafCallback.current = callback;

const step = useCallback((time: number) => {
if (rafActivity.current) {
rafCallback.current(time);
raf.current = requestAnimationFrame(step);
}
}, []);

const result = useMemo(() => ([
() => { // stop
if (rafActivity.current) {
rafActivity.current = false;
raf.current && cancelAnimationFrame(raf.current);
}
},
() => { // start
if (!rafActivity.current) {
rafActivity.current = true;
raf.current = requestAnimationFrame(step);
}
},
(): boolean => rafActivity.current // isActive
// eslint-disable-next-line react-hooks/exhaustive-deps
] as RafLoopReturns), []);

useEffect(() => {
if (initiallyActive) {
result[1]();
}

return result[0];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return result;
}
  • 1.用ref来缓存raf控制器;
  • 2.用useMemo()来缓存控制方法;
  • 3.组件销毁时会取消raf;

useError

抛出异常控制。

语法:

1
const dispatchError = useError();

参数:无

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useError } from 'react-use';

const Demo = () => {
const dispatchError = useError();

const clickHandler = () => {
dispatchError(new Error('Some error!'));
};

return <button onClick={clickHandler}>Click me to throw</button>;
};

// In parent app
const App = () => (
<ErrorBoundary>
<Demo />
</ErrorBoundary>
);

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/useError.ts

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

const useError = (): ((err: Error) => void) => {
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
if (error) {
throw error;
}
}, [error]);

const dispatchError = useCallback((err: Error) => {
setError(err);
}, []);

return dispatchError;
};

export default useError;

throw异常。

useFavicon

设置浏览器标题图标。

语法:

1
useFavicon(iconUrl)

参数:

  • iconUrl:{String},图标url地址

使用:

1
2
3
4
5
6
7
import {useFavicon} from 'react-use';

const Demo = () => {
useFavicon('https://cdn.sstatic.net/Sites/stackoverflow/img/favicon.ico');

return null;
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/useFavicon.ts

import { useEffect } from 'react';

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

export default useFavicon;

简单来说,就是设置属性是icon的link标签的引用地址。

*usePermission

浏览器API兼容状态的信息获取(基于navigator.permissions

语法:

1
const state = usePermission({ name: 'microphone' });

参数:

  • options:{Object},参数
    • name:{String},API字段

使用:

1
2
3
4
5
6
7
8
9
10
11
import {usePermission} from 'react-use';

const Demo = () => {
const state = usePermission({ name: 'microphone' });

return (
<pre>
{JSON.stringify(state, null, 2)}
</pre>
);
};

源码及解析
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
40
41
42
43
44
45
46
47
48
49
50
51
// src/usePermission.ts

import { useEffect, useState } from 'react';
import { off, on } from './util';

type PermissionDesc =
| PermissionDescriptor
| DevicePermissionDescriptor
| MidiPermissionDescriptor
| PushPermissionDescriptor;

type State = PermissionState | '';

const noop = () => {};

const usePermission = (permissionDesc: PermissionDesc): State => {
let mounted = true;
let permissionStatus: PermissionStatus | null = null;

const [state, setState] = useState<State>('');

const onChange = () => {
if (mounted && permissionStatus) {
setState(permissionStatus.state);
}
};

const changeState = () => {
onChange();
on(permissionStatus, 'change', onChange);
};

useEffect(() => {
navigator.permissions
.query(permissionDesc)
.then(status => {
permissionStatus = status;
changeState();
})
.catch(noop);

return () => {
mounted = false;
permissionStatus && off(permissionStatus, 'change', onChange);
};
}, []);

return state;
};

export default usePermission;

核心还是navigator.permissions。不过由于兼容问题,基本不会用。

p-permissions_c.jpg

useBeforeUnload.md

页面将退出/即将重新加载时触发。

语法:

1
useBeforeUnload(enabled, message);

参数:

  • enabled:{Boolean},是否启用
  • message:{String},退出/将重启加载时的提示文案

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 布尔值使用
// *useToggle() 也是react-use提供的一个钩子函数,用于切换

import {useBeforeUnload} from 'react-use';

const Demo = () => {
const [dirty, toggleDirty] = useToggle(false);
useBeforeUnload(dirty, 'You have unsaved changes, are you sure?');

return (
<div>
{dirty && <p>Try to reload or close tab</p>}
<button onClick={() => toggleDirty()}>{dirty ? 'Disable' : 'Enable'}</button>
</div>
);
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方法是用

import {useBeforeUnload} from 'react-use';

const Demo = () => {
const [dirty, toggleDirty] = useToggle(false);
const dirtyFn = useCallback(() => {
return dirty;
}, [dirty]);
useBeforeUnload(dirtyFn, 'You have unsaved changes, are you sure?');

return (
<div>
{dirty && <p>Try to reload or close tab</p>}
<button onClick={() => toggleDirty()}>{dirty ? 'Disable' : 'Enable'}</button>
</div>
);
};
源码及解析
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
// src/useBeforeUnload.ts

import { useCallback, useEffect } from 'react';

const useBeforeUnload = (enabled: boolean | (() => boolean) = true, message?: string) => {
const handler = useCallback(
(event: BeforeUnloadEvent) => {
const finalEnabled = typeof enabled === 'function' ? enabled() : true;

if (!finalEnabled) {
return;
}

event.preventDefault();

if (message) {
event.returnValue = message;
}

return message;
},
[enabled, message]
);

useEffect(() => {
if (!enabled) {
return;
}

window.addEventListener('beforeunload', handler);

return () => window.removeEventListener('beforeunload', handler);
}, [enabled, handler]);
};

export default useBeforeUnload;
  • 1.核心基于beforeunload的事件和解除;
  • 2.enabled默认为true,当传入函数时则获取函数执行结果,闭包的作用。;
  • 3.考虑了IE的event.returnValue用于事件捕获。

beforeunload事件的兼容情况:
p-beforeunload_c.jpg

useLockBodyScroll

控制区域滚动条是否可以滚动。

语法:

1
useLockBodyScroll(locked = true, elementRef);

参数:

  • locked:{Boolean},是否锁定滚动,默认为true
  • elementRef:{DOMElement},滚动区域元素

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// *useToggle() 也是react-use提供的一个钩子函数,用于切换

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

const Demo = () => {
const [locked, toggleLocked] = useToggle(false)

useLockBodyScroll(locked);

return (
<div>
<button onClick={() => toggleLocked()}>
{locked ? 'Unlock' : 'Lock'}
</button>
</div>
);
};

源码及解析
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// src/useLockBodyScroll.ts

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

export function getClosestBody(el: Element | HTMLElement | HTMLIFrameElement | null): HTMLElement | null {
if (!el) {
return null;
} else if (el.tagName === 'BODY') {
return el as HTMLElement;
} else if (el.tagName === 'IFRAME') {
const document = (el as HTMLIFrameElement).contentDocument;
return document ? document.body : null;
} else if (!(el as HTMLElement).offsetParent) {
return null;
}

return getClosestBody((el as HTMLElement).offsetParent!);
}

function preventDefault(rawEvent: TouchEvent): boolean {
const e = rawEvent || window.event;
// Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).
if (e.touches.length > 1) return true;

if (e.preventDefault) e.preventDefault();

return false;
}

export interface BodyInfoItem {
counter: number;
initialOverflow: CSSStyleDeclaration['overflow'];
}

const isIosDevice =
typeof window !== 'undefined' &&
window.navigator &&
window.navigator.platform &&
/iP(ad|hone|od)/.test(window.navigator.platform);

const bodies: Map<HTMLElement, BodyInfoItem> = new Map();

const doc: Document | undefined = typeof document === 'object' ? document : undefined;

let documentListenerAdded = false;

export default !doc
? function useLockBodyMock(_locked: boolean = true, _elementRef?: RefObject<HTMLElement>) {}
: function useLockBody(locked: boolean = true, elementRef?: RefObject<HTMLElement>) {
elementRef = elementRef || useRef(doc!.body);

const lock = body => {
const bodyInfo = bodies.get(body);
if (!bodyInfo) {
bodies.set(body, { counter: 1, initialOverflow: body.style.overflow });
if (isIosDevice) {
if (!documentListenerAdded) {
document.addEventListener('touchmove', preventDefault, { passive: false });

documentListenerAdded = true;
}
} else {
body.style.overflow = 'hidden';
}
} else {
bodies.set(body, { counter: bodyInfo.counter + 1, initialOverflow: bodyInfo.initialOverflow });
}
};

const unlock = body => {
const bodyInfo = bodies.get(body);
if (bodyInfo) {
if (bodyInfo.counter === 1) {
bodies.delete(body);
if (isIosDevice) {
body.ontouchmove = null;

if (documentListenerAdded) {
document.removeEventListener('touchmove', preventDefault);
documentListenerAdded = false;
}
} else {
body.style.overflow = bodyInfo.initialOverflow;
}
} else {
bodies.set(body, { counter: bodyInfo.counter - 1, initialOverflow: bodyInfo.initialOverflow });
}
}
};

useEffect(() => {
const body = getClosestBody(elementRef!.current);
if (!body) {
return;
}
if (locked) {
lock(body);
} else {
unlock(body);
}
}, [locked, elementRef.current]);

// clean up, on un-mount
useEffect(() => {
const body = getClosestBody(elementRef!.current);
if (!body) {
return;
}
return () => {
unlock(body);
};
}, []);
};
  • 1.<body><iframe>元素会默认本身为滚动元素,其他元素则会向上层递归寻找滚动元素,这也是为何官方会推荐使用<body><iframe>作为控制元素的原因,因为它们没有parent;
  • 2.滚动的锁定控制会根据平台来区分处理,iOS是通过touchmove事件来控制,而其他平台是通过设置元素的overflow样式属性控制;

useCopyToClipboard

用户剪切板的拷贝控制。

语法:

1
const [{value, error, noUserInteraction}, copyToClipboard] = useCopyToClipboard();

参数:无

返回:

  • dataObj:{Object},返回字段
    • value:{String},已经被拷贝的值;
    • error:{String},是否被捕获异常;
    • noUserInteraction:{Boolean},用户交互拷贝的值是否暴露给copy-to-clipboard这个库

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Demo = () => {
const [text, setText] = React.useState('');
const [state, copyToClipboard] = useCopyToClipboard();

return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="button" onClick={() => copyToClipboard(text)}>copy text</button>
{state.error
? <p>Unable to copy value: {state.error.message}</p>
: state.value && <p>Copied {state.value}</p>}
</div>
)
}

源码及解析
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// src/useCopyToClipboard.ts

import writeText from 'copy-to-clipboard';
import { useCallback } from 'react';
import useMountedState from './useMountedState';
import useSetState from './useSetState';

export interface CopyToClipboardState {
value?: string;
noUserInteraction: boolean;
error?: Error;
}

const useCopyToClipboard = (): [CopyToClipboardState, (value: string) => void] => {
const isMounted = useMountedState();
const [state, setState] = useSetState<CopyToClipboardState>({
value: undefined,
error: undefined,
noUserInteraction: true,
});

const copyToClipboard = useCallback(value => {
if (!isMounted()) {
return;
}
let noUserInteraction;
let normalizedValue;
try {
// only strings and numbers casted to strings can be copied to clipboard
if (typeof value !== 'string' && typeof value !== 'number') {
const error = new Error(`Cannot copy typeof ${typeof value} to clipboard, must be a string`);
if (process.env.NODE_ENV === 'development') console.error(error);
setState({
value,
error,
noUserInteraction: true,
});
return;
}
// empty strings are also considered invalid
else if (value === '') {
const error = new Error(`Cannot copy empty string to clipboard.`);
if (process.env.NODE_ENV === 'development') console.error(error);
setState({
value,
error,
noUserInteraction: true,
});
return;
}
normalizedValue = value.toString();
noUserInteraction = writeText(normalizedValue);
setState({
value: normalizedValue,
error: undefined,
noUserInteraction,
});
} catch (error) {
setState({
value: normalizedValue,
error,
noUserInteraction,
});
}
}, []);

return [state, copyToClipboard];
};

export default useCopyToClipboard;
  • 1.整个拷贝的处理依赖于copy-to-clipboard,其核心是document.execCommand()

p-execCommand_c.jpg

  • 2.捕获异常通过try...catch...
  • 3.useMountedState()也是react-use提供的一个钩子函数,用于获取组件mount状态信息;

useDebounce

防抖。

语法:

1
2
3
4
const [
isReady: () => boolean | null,
cancel: () => void,
] = useDebounce(fn: Function, ms: number, deps: DependencyList = []);

参数:

  • fn:{Function},回调函数;
  • ms:{Number},延迟时间,毫秒;
  • deps:{Array},依赖列表,等同于useEffect的第二个参数,可选;

返回:

  • isReady:()=> boolean|null。当前防抖函数的执行状态。
    • false:pending
    • true:called
    • null:cancelled
  • cancel: ()=>void。取消执行

使用:

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

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/useDebounce.ts

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];
}
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
40
41
42
// src/useTimeoutFn.ts

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

const isReady = useCallback(() => ready.current, []);

const set = useCallback(() => {
ready.current = false;
timeout.current && clearTimeout(timeout.current);

timeout.current = setTimeout(() => {
ready.current = true;
callback.current();
}, ms);
}, [ms]);

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

// update ref when function changes
useEffect(() => {
callback.current = fn;
}, [fn]);

// set on mount, clear on unmount
useEffect(() => {
set();

return clear;
}, [ms]);

return [isReady, clear, set];
}
  • 1.定时器单独封装成了一个工具hook:useTimeoutFn;
  • 2.useTimeoutFn中用useRef来缓存定时器返回的定时器id,避免hook的作用域副作用;

useThrottle

节流。

语法:

1
2
useThrottle(value, ms?: number);
useThrottleFn(fn, ms, args);

参数:

  • value:{any},节流的内容
  • ms:{Number},节流延迟时间,单位为毫秒
  • args:{Array},依赖列表,等同于useEffect的第二个参数,可选;

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState } from 'react';
import { useThrottle, useThrottleFn } from 'react-use';

const Demo = ({value}) => {
const throttledValue = useThrottle(value);
// const throttledValue = useThrottleFn(value => value, 200, [value]);

return (
<>
<div>Value: {value}</div>
<div>Throttled value: {throttledValue}</div>
</>
);
};

源码及解析
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
// src/useThrottle.ts

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;

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 {
nextValue.current = value;
hasNextValue.current = true;
}
}, [value]);

useUnmount(() => {
timeout.current && clearTimeout(timeout.current);
});

return state;
};

export default useThrottle;
  • useUnmount主要实现了组件即将销毁时的回调,在useThrottleFn中主要作用是触发组件销毁时的定时器取消;
  • useThrottle较为简单,用useEffect来控制节流触发,以value参数作为控制变量。
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
// src/useThrottleFn.ts

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

const useThrottleFn = <T, U extends any[]>(fn: (...args: U) => T, ms: number = 200, args: U) => {
const [state, setState] = useState<T | null>(null);
const timeout = useRef<ReturnType<typeof setTimeout>>();
const nextArgs = useRef<U>();

useEffect(() => {
if (!timeout.current) {
setState(fn(...args));
const timeoutCallback = () => {
if (nextArgs.current) {
setState(fn(...nextArgs.current));
nextArgs.current = undefined;
timeout.current = setTimeout(timeoutCallback, ms);
} else {
timeout.current = undefined;
}
};
timeout.current = setTimeout(timeoutCallback, ms);
} else {
nextArgs.current = args;
}
}, args);

useUnmount(() => {
timeout.current && clearTimeout(timeout.current);
});

return state;
};

export default useThrottleFn;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/useUnmount.ts

import { useRef } from 'react';
import useEffectOnce from './useEffectOnce';

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;

useEffectOnce(() => () => fnRef.current());
};

export default useUnmount;
1
2
3
4
5
6
7
8
// src/useEffectOnce.ts
import { EffectCallback, useEffect } from 'react';

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

export default useEffectOnce;
  • useThrottleFn与useThrottle类似,主要控制了函数的触发。

useAsyncFn

异步方法钩子。

语法:

1
const [state, fetch] = useAsyncFn<Result, Args>(fn, deps?: any[], initialState?: AsyncState<Result>);

参数:

  • fn:{Function},异步函数
  • args:{Array},依赖列表,等同于useEffect的第二个参数,可选;
  • initialState:{any},初始值

返回:

  • state:{Object}
    • loading:{Boolean},是否处于pending
    • error:{Error},是否抛出了异常
    • value:{String},返回值
  • fetch:开始执行方法

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {useAsyncFn} from 'react-use';

const Demo = ({url}) => {
const [state, fetch] = 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={() => fetch()}>Start loading</button>
</div>
);
};

源码及解析
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// src/useAsyncFn.ts
import { DependencyList, useCallback, useState, useRef } from 'react';
import useMountedState from './useMountedState';
import { FnReturningPromise, PromiseType } from './util';

export type AsyncState<T> =
| {
loading: boolean;
error?: undefined;
value?: undefined;
}
| {
loading: true;
error?: Error | undefined;
value?: T;
}
| {
loading: false;
error: Error;
value?: undefined;
}
| {
loading: false;
error?: undefined;
value: T;
};

type StateFromFnReturningPromise<T extends FnReturningPromise> = AsyncState<PromiseType<ReturnType<T>>>;

export type AsyncFnReturn<T extends FnReturningPromise = FnReturningPromise> = [StateFromFnReturningPromise<T>, T];

export default function useAsyncFn<T extends FnReturningPromise>(
fn: T,
deps: DependencyList = [],
initialState: StateFromFnReturningPromise<T> = { loading: false }
): AsyncFnReturn<T> {
const lastCallId = useRef(0);
const isMounted = useMountedState();
const [state, set] = useState<StateFromFnReturningPromise<T>>(initialState);

const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
const callId = ++lastCallId.current;
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];
}
  • 从util库中引用的FnReturningPromise, PromiseType只是对类型的定义声明;

useAsync

异步钩子。

语法:

1
const state = useAsync(fn, args?: any[]);

参数:

  • fn:{Function},异步函数
  • args:{Array},依赖列表,等同于useEffect的第二个参数,可选;

返回:

  • state:{Object}
    • loading:{Boolean},是否处于pending
    • error:{Error},是否抛出了异常
    • value:{String},返回值

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {useAsync} from 'react-use';

const Demo = ({url}) => {
const state = useAsync(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>
}
</div>
);
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/useAsync.ts

import { DependencyList, useEffect } from 'react';
import useAsyncFn from './useAsyncFn';
import { FnReturningPromise } from './util';

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

export default function useAsync<T extends FnReturningPromise>(fn: T, deps: DependencyList = []) {
const [state, callback] = useAsyncFn(fn, deps, {
loading: true,
});

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

return state;
}

就是在useAsyncFn之上做了一层封装。

useAsyncRetry

带取消功能的异步钩子。

语法:

1
const state = useAsyncRetry(fn, args?: any[]);

参数:

  • fn:{Function},异步函数
  • args:{Array},依赖列表,等同于useEffect的第二个参数,可选;

返回:

  • state:{Object}
    • loading:{Boolean},是否处于pending
    • error:{Error},是否抛出了异常
    • value:{String},返回值
    • retry:{Function},取消方法

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {useAsyncRetry} from 'react-use';

const Demo = ({url}) => {
const state = useAsyncRetry(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>
}
{!loading && <button onClick={() => state.retry()}>Start loading</button>}
</div>
);
};

源码及解析
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
// src/useAsyncRetry.ts
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;
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 };
};

export default useAsyncRetry;

基于useAsync的封装,通过封装回调方法来控制退出方法。

生命周期

useEffectOnce

只执行一次的effect钩子。

语法:

1
useEffectOnce(effect: EffectCallback);

参数:

  • effect:{Function},副作用回调

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
import {useEffectOnce} from 'react-use';

const Demo = () => {
useEffectOnce(() => {
console.log('Running effect once on mount')

return () => {
console.log('Running clean-up of effect on unmount')
}
});

return null;
};

源码及解析
1
2
3
4
5
6
7
// src/useEffectOnce.ts

import { EffectCallback, useEffect } from 'react';
const useEffectOnce = (effect: EffectCallback) => {
useEffect(effect, []);
};
export default useEffectOnce;

不需要解释。。。

useEvent

事件处理。

语法:

1
2
useEvent(event, handler)
useEvent(event, handler, target, options)

参数:

  • event:{String},事件名称;
  • handler:{function},事件函数;
  • target:{DOMElement|Window},事件节点;
  • options:{Object},参数。一个指定有关 listener 属性的可选参数对象。可用的选项如下:
    • capture:{Boolean},表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
    • once:{Boolean},表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
    • passive:{Boolean},设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。查看 使用 passive 改善的滚屏性能 了解更多。
    • mozSystemGroup: {Boolean},只能在 XBL 或者是 Firefox’ chrome 使用,表示 listener 被添加到 system group。

使用:

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
// *useList也是react-use提供的一个钩子函数,用于在函数组件中控制数组

import {useEvent, useList} from 'react-use';

const Demo = () => {
const [list, {push, clear}] = useList();

const onKeyDown = useCallback(({key}) => {
if (key === 'r') clear();
push(key);
}, []);

useEvent('keydown', onKeyDown);

return (
<div>
<p>
Press some keys on your keyboard, <code style={{color: 'tomato'}}>r</code> key resets the list
</p>
<pre>
{JSON.stringify(list, null, 4)}
</pre>
</div>
);
};

源码及解析
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// src/useEvent.ts

import { useEffect } from 'react';
import { isClient } from './util';

export interface ListenerType1 {
addEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
removeEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
}

export interface ListenerType2 {
on(name: string, handler: (event?: any) => void, ...args: any[]);
off(name: string, handler: (event?: any) => void, ...args: any[]);
}

export type UseEventTarget = ListenerType1 | ListenerType2;

const defaultTarget = isClient ? window : null;

const isListenerType1 = (target: any): target is ListenerType1 => {
return !!target.addEventListener;
};
const isListenerType2 = (target: any): target is ListenerType2 => {
return !!target.on;
};

type AddEventListener<T> = T extends ListenerType1 ? T['addEventListener'] : T extends ListenerType2 ? T['on'] : never;

const useEvent = <T extends UseEventTarget>(
name: Parameters<AddEventListener<T>>[0],
handler?: null | undefined | Parameters<AddEventListener<T>>[1],
target: null | T | Window = defaultTarget,
options?: Parameters<AddEventListener<T>>[2]
) => {
useEffect(() => {
if (!handler) {
return;
}
if (!target) {
return;
}
if (isListenerType1(target)) {
target.addEventListener(name, handler, options);
} else if (isListenerType2(target)) {
target.on(name, handler, options);
}
return () => {
if (isListenerType1(target)) {
target.removeEventListener(name, handler, options);
} else if (isListenerType2(target)) {
target.off(name, handler, options);
}
};
}, [name, handler, target, JSON.stringify(options)]);
};

export default useEvent;
  • 1.考虑了IE的事件处理;
  • 2.考虑了非浏览器端的兼容;

useLifecycles

mount和unmount的生命周期封装。

语法:

1
useLifecycles(mount, unmount);

参数:

  • mount:{Function},mount执行回调
  • unmount:{Function},unmount执行回调

使用:

1
2
3
4
5
6
import {useLifecycles} from 'react-use';

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

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/useLifecycles.ts

import { useEffect } from 'react';

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

export default useLifecycles;

不需要解释。。。

useMountedState

组件状态回调钩子。

语法:

1
const isMounted = useMountedState();

返回:

  • isMounted:{Function},() => boolean,获取当前是否mount状态

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import * as React from 'react';
import {useMountedState} from 'react-use';

const Demo = () => {
const isMounted = useMountedState();

React.useEffect(() => {
setTimeout(() => {
if (isMounted()) {
// ...
} else {
// ...
}
}, 1000);
});
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/useMountedState.ts
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;
}

通过useEffect来控制mount状态,用ref防止作用域问题。

useUnmountPromise

语法:

1
2
3
const mounted = useUnmountPromise();

mounted(promise, onError);

返回:

  • mounted:{Function},执行函数。参数:
    • promise:{Function},回调函数
    • onError:{Function},异常捕获处理,可选

使用:

1
2
3
4
5
6
7
8
import useUnmountPromise from 'react-use/lib/useUnmountPromise';

const Demo = () => {
const mounted = useUnmountPromise();
useEffect(async () => {
await mounted(someFunction()); // Will not resolve if component un-mounts.
});
};

源码及解析
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
// src/useUnmountPromise.ts

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

export type Race = <P extends Promise<any>, E = any>(promise: P, onError?: (error: E) => void) => P;

const useUnmountPromise = (): Race => {
const refUnmounted = useRef(false);
useEffect(() => () => {
refUnmounted.current = true;
});

const wrapper = useMemo(() => {
const race = <P extends Promise<any>, E>(promise: P, onError?: (error: E) => void) => {
const newPromise: P = new Promise((resolve, reject) => {
promise.then(
result => {
if (!refUnmounted.current) resolve(result);
},
error => {
if (!refUnmounted.current) reject(error);
else if (onError) onError(error);
else console.error('useUnmountPromise', error);
}
);
}) as P;
return newPromise;
};
return race;
}, []);

return wrapper;
};

export default useUnmountPromise;

promise的处理+ref的缓存。

usePromise

用于处理函数组件中的包裹promise方法。并且此方法只有当前组件mount后才会resolve。

语法:

1
const mounted = usePromise();

返回:

  • mounted:{Function},包裹函数。参数:
    • promise:{Function},promise方法

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {usePromise} from 'react-use';

const Demo = ({promise}) => {
const mounted = usePromise();
const [value, setValue] = useState();

useEffect(() => {
(async () => {
const value = await mounted(promise);
// This line will not execute if <Demo> component gets unmounted.
setValue(value);
})();
});
};

源码及解析
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
// src/usePromise.ts

import { useCallback } from 'react';
import useMountedState from './useMountedState';

export type UsePromise = () => <T>(promise: Promise<T>) => Promise<T>;

const usePromise: UsePromise = () => {
const isMounted = useMountedState();
return useCallback(
(promise: Promise<any>) =>
new Promise<any>((resolve, reject) => {
const onValue = value => {
isMounted() && resolve(value);
};
const onError = error => {
isMounted() && reject(error);
};
promise.then(onValue, onError);
}),
[]
);
};

export default usePromise;

简单根据当前组件跟mount状态来进行控制。

useLogger

包含当前生命周期状态的log控制。

语法:

1
useLogger(componentName: string, ...rest);

参数:

  • componentName:{String},log内容

使用:

1
2
3
4
5
6
import {useLogger} from 'react-use';

const Demo = (props) => {
useLogger('Demo', props);
return null;
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/useLogger.ts

import useEffectOnce from './useEffectOnce';
import useUpdateEffect from './useUpdateEffect';

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

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

export default useLogger;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/useUpdateEffect

import { useEffect } from 'react';
import { useFirstMountState } from './useFirstMountState';

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

useEffect(() => {
if (!isFirstMount) {
return effect();
}
}, deps);
};

export default useUpdateEffect;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/useFirstMountState
import { useRef } from 'react';

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

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

return true;
}

return isFirst.current;
}
  • 1.useUpdateEffect是通过useFirstMountState的闭包变量进行的判断,实现控制非首次触发effect的效果(update);
  • 2.useEffectOnce用于实现mount和unmount的控制回调;

useMount

语法:

1
useMount(fn: () => void);

参数:

  • fn:{Function},回调函数;

使用:

1
2
3
4
5
6
import {useMount} from 'react-use';

const Demo = () => {
useMount(() => alert('MOUNTED'));
return null;
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
// src/useMount.ts

import useEffectOnce from './useEffectOnce';

const useMount = (fn: () => void) => {
useEffectOnce(() => {
fn();
});
};

export default useMount;

不需要解释。。。

useUpdateEffect

组件更新时的effect。

语法:

1
useUpdateEffect(fn, deps)

参数:

  • fn:{Function},回调函数;
  • deps:[Array],依赖项;

使用:

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
import React from 'react'
import {useUpdateEffect} from 'react-use';

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

React.useEffect(() => {
const interval = setInterval(() => {
setCount(count => count + 1)
}, 1000)

return () => {
clearInterval(interval)
}
}, [])

useUpdateEffect(() => {
console.log('count', count) // will only show 1 and beyond

return () => { // *OPTIONAL*
// do something on unmount
}
}) // you can include deps array if necessary

return <div>Count: {count}</div>
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/useUpdateEffect.ts

import { useEffect } from 'react';
import { useFirstMountState } from './useFirstMountState';

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

useEffect(() => {
if (!isFirstMount) {
return effect();
}
}, deps);
};

export default useUpdateEffect;

useUpdateEffect是通过useFirstMountState的闭包变量进行的判断,实现控制非首次触发effect的效果(update)

useIsomorphicLayoutEffect

根据环境判断使用layoutEffect/effect。

语法:

1
useIsomorphicLayoutEffect(effect: EffectCallback, deps?: ReadonlyArray<any> | undefined);

参数:

  • effect:{Function},回调
  • deps:{Array},依赖列表,等同于useEffect的第二个参数,可选;

使用:

1
2
3
4
5
6
7
8
9
import {useIsomorphicLayoutEffect} from 'react-use';

const Demo = ({value}) => {
useIsomorphicLayoutEffect(() => {
window.console.log(value)
}, [value]);

return null;
};

源码及解析
1
2
3
4
5
6
7
// src/useIsomorphicLayoutEffect

import { useEffect, useLayoutEffect } from 'react';

const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;

export default useIsomorphicLayoutEffect;

根据执行环境进行了区分使用。

useCustomCompareEffect

通过依赖项的比较来控制effect执行。

语法:

1
useCustomCompareEffect(effect: () => void | (() => void | undefined), deps: any[], depsEqual: (prevDeps: any[], nextDeps: any[]) => boolean);

  • effect:{Function},副作用回调
  • deps:{Array},依赖列表;
  • depsEqual:{Function},比较函数;参数
    • prevDeps:{Array},比较前依赖项
    • nextDeps:{Array},比较后依赖项;
      返回:{Boolean},是否需要执行副作用

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {useCounter, useCustomCompareEffect} from 'react-use';
import isEqual from 'lodash/isEqual';

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

useCustomCompareEffect(() => {
inc(options.step)
}, [options], (prevDeps, nextDeps) => isEqual(prevDeps, nextDeps));

return (
<div>
<p>useCustomCompareEffect with deep comparison: {count}</p>
</div>
);
};

源码及解析
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
// src/useCustomCompareEffect

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>
) => {
if (process.env.NODE_ENV !== 'production') {
if (!(deps instanceof Array) || !deps.length) {
console.warn('`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.');
}

if (deps.every(isPrimitive)) {
console.warn(
'`useCustomCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.'
);
}

if (typeof depsEqual !== 'function') {
console.warn('`useCustomCompareEffect` should be used with depsEqual callback for comparing deps list');
}
}

const ref = useRef<TDeps | undefined>(undefined);

if (!ref.current || !depsEqual(deps, ref.current)) {
ref.current = deps;
}

useEffect(effect, ref.current);
};

export default useCustomCompareEffect;
  • 1.用ref来控制useEffect的缓存依赖项
  • 2.如果依赖项deps都是原始数据类型的话不推荐使用此钩子

useDeepCompareEffect

使用深比较effect。

语法:

1
useDeepCompareEffect(effect: () => void | (() => void | undefined), deps: any[]);

参数:

  • effect:{Function},副作用回调
  • deps:{Array},依赖列表,等同于useEffect的第二个参数,可选;

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {useCounter, useDeepCompareEffect} from 'react-use';

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

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

return (
<div>
<p>useDeepCompareEffect: {count}</p>
</div>
);
};

源码及解析
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
// src/useDeepCompareEffect

import { DependencyList, EffectCallback } from 'react';
import { isDeepEqual } from './util'; //import isDeepEqualReact from 'fast-deep-equal/react';
import useCustomCompareEffect from './useCustomCompareEffect';

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

const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
if (process.env.NODE_ENV !== 'production') {
if (!(deps instanceof Array) || !deps.length) {
console.warn('`useDeepCompareEffect` should not be used with no dependencies. Use React.useEffect instead.');
}

if (deps.every(isPrimitive)) {
console.warn(
'`useDeepCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.'
);
}
}

useCustomCompareEffect(effect, deps, isDeepEqual);
};

export default useDeepCompareEffect;
  • 1.核心的深比较函数依赖于fast-deep-equal
  • 2.如果依赖项deps都是原始数据类型的话不推荐使用此钩子

useShallowCompareEffect

使用浅比较effect。

语法:

1
useShallowCompareEffect(effect: () => void | (() => void | undefined), deps: any[]);

参数:

  • effect:{Function},副作用回调
  • deps:{Array},依赖列表,等同于useEffect的第二个参数,可选;

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {useCounter, useShallowCompareEffect} from 'react-use';

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

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

return (
<div>
<p>useShallowCompareEffect: {count}</p>
</div>
);
};

源码及解析
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
// src/useShallowCompareEffect.ts

import { DependencyList, EffectCallback } from 'react';
import { equal as isShallowEqual } from 'fast-shallow-equal';
import useCustomCompareEffect from './useCustomCompareEffect';

const isPrimitive = (val: any) => val !== Object(val);
const shallowEqualDepsList = (prevDeps: DependencyList, nextDeps: DependencyList) =>
prevDeps.every((dep, index) => isShallowEqual(dep, nextDeps[index]));

const useShallowCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
if (process.env.NODE_ENV !== 'production') {
if (!(deps instanceof Array) || !deps.length) {
console.warn('`useShallowCompareEffect` should not be used with no dependencies. Use React.useEffect instead.');
}

if (deps.every(isPrimitive)) {
console.warn(
'`useShallowCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.'
);
}
}

useCustomCompareEffect(effect, deps, shallowEqualDepsList);
};

export default useShallowCompareEffect;
  • 1.核心的浅比较函数依赖于fast-shallow-equal
  • 2.如果依赖项deps都是原始数据类型的话不推荐使用此钩子

useRaf

控制requestAnimationFrame。

语法:

1
const elapsed = useRaf(ms?: number, delay?: number): number;

参数:

  • ms:{Number},动画总时间,毫秒。默认是1e12,即1e9秒;
  • delay:{Number},延迟时间,毫秒。可选;

返回:

  • elapsed:{Number},运行动画百分比

使用:

1
2
3
4
5
6
7
8
9
10
11
import {useRaf} from 'react-use';

const Demo = () => {
const elapsed = useRaf(5000, 1000);

return (
<div>
Elapsed: {elapsed}
</div>
);
};

源码及解析
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
40
41
42
// src/useRaf.ts

import { useState } from 'react';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';

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

export default useRaf;

比较简单,通过requestAnimationFrame和setTimeout来控制整个运作。

useInterval

setInterval的控制钩子。为何要单独抽离成一个钩子,可以见Dan Abramov’s article on overreacted.io,写得非常好了。

语法:

1
useInterval(callback, delay?: number)

参数:

  • callback:{Function},回调函数;
  • delay:{Number},定时器间隔,默认为0。

使用:

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
import * as React from 'react';
import {useInterval} from 'react-use';

const Demo = () => {
const [count, setCount] = React.useState(0);
const [delay, setDelay] = React.useState(1000);
const [isRunning, toggleIsRunning] = useBoolean(true);

useInterval(
() => {
setCount(count + 1);
},
isRunning ? delay : null
);

return (
<div>
<div>
delay: <input value={delay} onChange={event => setDelay(Number(event.target.value))} />
</div>
<h1>count: {count}</h1>
<div>
<button onClick={toggleIsRunning}>{isRunning ? 'stop' : 'start'}</button>
</div>
</div>
);
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/useInterval.ts

import { useEffect, useRef } from 'react';

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

export default useInterval;

ref作为动画id避免函数组件的作用域问题。

useHarmonicIntervalFn

同样是setInterval的控制钩子,但是所有的effect会在同样的延迟进行。

语法:

1
useHarmonicIntervalFn(fn, delay?: number)

参数:

  • fn:{Function},回调函数;
  • delay:{Number},定时器间隔,默认为0。

使用:

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
import * as React from 'react';
import {useHarmonicIntervalFn} from 'react-use';

const Demo = () => {
const [count, setCount] = React.useState(0);
const [delay, setDelay] = React.useState(1000);
const [isRunning, toggleIsRunning] = useBoolean(true);

useHarmonicIntervalFn(
() => {
setCount(count + 1);
},
isRunning ? delay : null
);

return (
<div>
<div>
delay: <input value={delay} onChange={event => setDelay(Number(event.target.value))} />
</div>
<h1>count: {count}</h1>
<div>
<button onClick={toggleIsRunning}>{isRunning ? 'stop' : 'start'}</button>
</div>
</div>
);
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/useHarmonicIntervalFn.ts

import { useEffect, useRef } from 'react';
import { setHarmonicInterval, clearHarmonicInterval } from 'set-harmonic-interval';

const useHarmonicIntervalFn = (fn: Function, delay: number | null = 0) => {
const latestCallback = useRef<Function>(() => {});

useEffect(() => {
latestCallback.current = fn;
});

useEffect(() => {
if (delay !== null) {
const interval = setHarmonicInterval(() => latestCallback.current(), delay);
return () => clearHarmonicInterval(interval);
}
return undefined;
}, [delay]);
};

export default useHarmonicIntervalFn;

核心的实现依赖于set-harmonic-interval这个库。

useSpring

弹簧力学的动力钩子。需要安装rebound

语法:

1
const currentValue = useSpring(targetValue, tension, friction);

参数:

  • targetValue:{Number},目标值。可选,默认为0;
  • tension:{Number},张力。可选,默认为50;
  • friction:{Number},摩擦力。可选,默认为50。

返回:

  • currentValue:{Number},当前值

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import useSpring from 'react-use/lib/useSpring';

const Demo = () => {
const [target, setTarget] = useState(50);
const value = useSpring(target);

return (
<div>
{value}
<br />
<button onClick={() => setTarget(0)}>Set 0</button>
<button onClick={() => setTarget(100)}>Set 100</button>
</div>
);
};

源码及解析
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
40
41
42
43
44
45
46
47
// useSpring

import { useEffect, useMemo, useState } from 'react';
import { Spring, SpringSystem } from 'rebound';

const useSpring = (targetValue: number = 0, tension: number = 50, friction: number = 3) => {
const [spring, setSpring] = useState<Spring | null>(null);
const [value, setValue] = useState<number>(targetValue);

// memoize listener to being able to unsubscribe later properly, otherwise
// listener fn will be different on each re-render and wouldn't unsubscribe properly.
const listener = useMemo(
() => ({
onSpringUpdate: currentSpring => {
const newValue = currentSpring.getCurrentValue();
setValue(newValue);
},
}),
[]
);

useEffect(() => {
if (!spring) {
const newSpring = new SpringSystem().createSpring(tension, friction);
newSpring.setCurrentValue(targetValue);
setSpring(newSpring);
newSpring.addListener(listener);
}

return () => {
if (spring) {
spring.removeListener(listener);
setSpring(null);
}
};
}, [tension, friction, spring]);

useEffect(() => {
if (spring) {
spring.setEndValue(targetValue);
}
}, [targetValue]);

return value;
};

export default useSpring;

对rebound的封装。

useTimeoutFn

控制setTimeout。

语法:

1
2
3
4
5
const [
isReady: () => boolean | null,
cancel: () => void,
reset: () => void,
] = useTimeoutFn(fn: Function, ms: number = 0);

参数:

  • fn:{Function},回调方法;
  • ms:{Number},延迟时间,毫秒。默认是0;

返回:

  • functions:{Function[]},控制方法集合
    • 0:() => boolean | null,当前是否已经完成;
    • 1:() => void,取消方法;
    • 2:() => void,重置方法

使用:

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
import * as React from 'react';
import { useTimeoutFn } from 'react-use';

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

function fn() {
setState(`called at ${Date.now()}`);
}

const [isReady, cancel, reset] = useTimeoutFn(fn, 5000);
const cancelButtonClick = useCallback(() => {
if (isReady() === false) {
cancel();
setState(`cancelled`);
} else {
reset();
setState('Not called yet');
}
}, []);

const readyState = isReady();

return (
<div>
<div>{readyState !== null ? 'Function will be called in 5 seconds' : 'Timer cancelled'}</div>
<button onClick={cancelButtonClick}> {readyState === false ? 'cancel' : 'restart'} timeout</button>
<br />
<div>Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled'}</div>
<div>{state}</div>
</div>
);
};

源码及解析
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
40
41
42
// src/useTimeoutFn

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

const isReady = useCallback(() => ready.current, []);

const set = useCallback(() => {
ready.current = false;
timeout.current && clearTimeout(timeout.current);

timeout.current = setTimeout(() => {
ready.current = true;
callback.current();
}, ms);
}, [ms]);

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

// update ref when function changes
useEffect(() => {
callback.current = fn;
}, [fn]);

// set on mount, clear on unmount
useEffect(() => {
set();

return clear;
}, [ms]);

return [isReady, clear, set];
}

ref作为定时器id用来避免作用域问题。并且考虑了unmount时的定时器清除。

useTimeout

控制setTimeout。

语法:

1
2
3
4
5
const [
isReady: () => boolean | null,
cancel: () => void,
reset: () => void,
] = useTimeout(ms: number = 0);

参数:

  • ms:{Number},延迟时间,毫秒;

返回:

  • functions:{Function[]},控制方法集合
    • 0:() => boolean | null,当前是否已经完成;
    • 1:() => void,取消方法;
    • 2:() => void,重置方法

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useTimeout } from 'react-use';

function TestComponent(props: { ms?: number } = {}) {
const ms = props.ms || 5000;
const [isReady, cancel] = useTimeout(ms);

return (
<div>
{ isReady() ? 'I\'m reloaded after timeout' : `I will be reloaded after ${ ms / 1000 }s` }
{ isReady() === false ? <button onClick={ cancel }>Cancel</button> : '' }
</div>
);
}

const Demo = () => {
return (
<div>
<TestComponent />
<TestComponent ms={ 10000 } />
</div>
);
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
// src/useTimeout.ts

import useTimeoutFn from './useTimeoutFn';
import useUpdate from './useUpdate';

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

export default function useTimeout(ms: number = 0): UseTimeoutReturn {
const update = useUpdate();

return useTimeoutFn(update, ms);
}

基于useTimeoutFn,实现组件延迟更新render。

useTween

动画过渡钩子。

语法:

1
const current = useTween(easing?: string, ms?: number, delay?: number): number

参数:

  • easing:{String},动画效果,字可见字段集合。默认为"inCirc"
  • ms:{Number},动画总时间,毫秒。默认是200;
  • delay:{Number},延迟时间,毫秒。默认是0;

返回:

  • current:{Number},当前进度。

使用:

1
2
3
4
5
6
7
8
9
10
11
import {useTween} from 'react-use';

const Demo = () => {
const t = useTween();

return (
<div>
Tween: {t}
</div>
);
};

源码及解析
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
// src/useTween.ts

import { easing } from 'ts-easing';
import useRaf from './useRaf';

export type Easing = (t: number) => number;

const useTween = (easingName: string = 'inCirc', ms: number = 200, delay: number = 0): number => {
const fn: Easing = easing[easingName];
const t = useRaf(ms, delay);

if (process.env.NODE_ENV !== 'production') {
if (typeof fn !== 'function') {
console.error(
'useTween() expected "easingName" property to be a valid easing function name, like:' +
'"' +
Object.keys(easing).join('", "') +
'".'
);
console.trace();
return 0;
}
}

return fn(t);
};

export default useTween;

依赖于ts-easing动画库,动画控制核心依赖于requestAnimationFrame。

useUpdate

重新渲染组件。

语法:

1
const update = useUpdate();

返回:

  • update:{Function},更新函数

使用:

1
2
3
4
5
6
7
8
9
10
11
import {useUpdate} from 'react-use';

const Demo = () => {
const update = useUpdate();
return (
<>
<div>Time: {Date.now()}</div>
<button onClick={update}>Update</button>
</>
);
};

源码及解析
1
2
3
4
5
6
7
8
9
10
11
12
// src/useUpdate.ts

import { useReducer } from 'react';

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

const useUpdate = () => {
const [, update] = useReducer(updateReducer, 0);
return update as () => void;
};

export default useUpdate;
  • 依赖于useReducer,以计数的形式来触发组件的更新;
  • 1_000_000其实就是1000000