Dig deep into Hooks

推荐阅读

参考链接

Hooks Recipes

useEventListener

1
import { useState, useRef, useEffect, useCallback } from 'react';
2
3
// Usage
4
function App(){
5
// State for storing mouse coordinates
6
const [coords, setCoords] = useState({ x: 0, y: 0 });
7
8
// Event handler utilizing useCallback ...
9
// ... so that reference never changes.
10
const handler = useCallback(
11
({ clientX, clientY }) => {
12
// Update coordinates
13
setCoords({ x: clientX, y: clientY });
14
},
15
[setCoords]
16
);
17
18
// Add event listener using our hook
19
useEventListener('mousemove', handler);
20
21
return (
22
<h1>
23
The mouse position is ({coords.x}, {coords.y})
24
</h1>
25
);
26
}
27
28
// Hook
29
function useEventListener(eventName, handler, element = window){
30
// Create a ref that stores handler
31
const savedHandler = useRef();
32
33
// Update ref.current value if handler changes.
34
// This allows our effect below to always get latest handler ...
35
// ... without us needing to pass it in effect deps array ...
36
// ... and potentially cause effect to re-run every render.
37
useEffect(() => {
38
savedHandler.current = handler;
39
}, [handler]);
40
41
useEffect(
42
() => {
43
// Make sure element supports addEventListener
44
// On
45
const isSupported = element && element.addEventListener;
46
if (!isSupported) return;
47
48
// Create event listener that calls handler function stored in ref
49
const eventListener = event => savedHandler.current(event);
50
51
// Add event listener
52
element.addEventListener(eventName, eventListener);
53
54
// Remove event listener on cleanup
55
return () => {
56
element.removeEventListener(eventName, eventListener);
57
};
58
},
59
[eventName, element] // Re-run if eventName or element changes
60
);
61
};
Copied!

useWhyDidYouUpdate

1
import { useState, useEffect, useRef } from 'react';
2
3
// Let's pretend this <Counter> component is expensive to re-render so ...
4
// ... we wrap with React.memo, but we're still seeing performance issues :/
5
// So we add useWhyDidYouUpdate and check our console to see what's going on.
6
const Counter = React.memo(props => {
7
useWhyDidYouUpdate('Counter', props);
8
return <div style={props.style}>{props.count}</div>;
9
});
10
11
function App() {
12
const [count, setCount] = useState(0);
13
const [userId, setUserId] = useState(0);
14
15
// Our console output tells use that the style prop for <Counter> ...
16
// ... changes on every render, even when we only change userId state by ...
17
// ... clicking the "switch user" button. Oh of course! That's because the
18
// ... counterStyle object is being re-created on every render.
19
// Thanks to our hook we figured this out and realized we should probably ...
20
// ... move this object outside of the component body.
21
const counterStyle = {
22
fontSize: '3rem',
23
color: 'red'
24
};
25
26
return (
27
<div>
28
<div className="counter">
29
<Counter count={count} style={counterStyle} />
30
<button onClick={() => setCount(count + 1)}>Increment</button>
31
</div>
32
<div className="user">
33
<img src={`http://i.pravatar.cc/80?img=${userId}`} />
34
<button onClick={() => setUserId(userId + 1)}>Switch User</button>
35
</div>
36
</div>
37
);
38
}
39
40
// Hook
41
function useWhyDidYouUpdate(name, props) {
42
// Get a mutable ref object where we can store props ...
43
// ... for comparison next time this hook runs.
44
const previousProps = useRef();
45
46
useEffect(() => {
47
if (previousProps.current) {
48
// Get all keys from previous and current props
49
const allKeys = Object.keys({ ...previousProps.current, ...props });
50
// Use this object to keep track of changed props
51
const changesObj = {};
52
// Iterate through keys
53
allKeys.forEach(key => {
54
// If previous is different from current
55
if (previousProps.current[key] !== props[key]) {
56
// Add to changesObj
57
changesObj[key] = {
58
from: previousProps.current[key],
59
to: props[key]
60
};
61
}
62
});
63
64
// If changesObj not empty then output to console
65
if (Object.keys(changesObj).length) {
66
console.log('[why-did-you-update]', name, changesObj);
67
}
68
}
69
70
// Finally update previousProps with current props for next hook call
71
previousProps.current = props;
72
});
73
}
Copied!

useKeyPress

1
import { useState, useEffect } from 'react';
2
3
// Usage
4
function App() {
5
// Call our hook for each key that we'd like to monitor
6
const happyPress = useKeyPress('h');
7
const sadPress = useKeyPress('s');
8
const robotPress = useKeyPress('r');
9
const foxPress = useKeyPress('f');
10
11
return (
12
<div>
13
<div>h, s, r, f</div>
14
<div>
15
{happyPress && '😊'}
16
{sadPress && '😢'}
17
{robotPress && '🤖'}
18
{foxPress && '🦊'}
19
</div>
20
</div>
21
);
22
}
23
24
// Hook
25
function useKeyPress(targetKey) {
26
// State for keeping track of whether key is pressed
27
const [keyPressed, setKeyPressed] = useState(false);
28
29
// If pressed key is our target key then set to true
30
function downHandler({ key }) {
31
if (key === targetKey) {
32
setKeyPressed(true);
33
}
34
}
35
36
// If released key is our target key then set to false
37
const upHandler = ({ key }) => {
38
if (key === targetKey) {
39
setKeyPressed(false);
40
}
41
};
42
43
// Add event listeners
44
useEffect(() => {
45
window.addEventListener('keydown', downHandler);
46
window.addEventListener('keyup', upHandler);
47
// Remove event listeners on cleanup
48
return () => {
49
window.removeEventListener('keydown', downHandler);
50
window.removeEventListener('keyup', upHandler);
51
};
52
}, []); // Empty array ensures that effect is only run on mount and unmount
53
54
return keyPressed;
55
}
Copied!

useOnScreen

1
import { useState, useEffect, useRef } from 'react';
2
3
// Usage
4
function App() {
5
// Ref for the element that we want to detect whether on screen
6
const ref = useRef();
7
// Call the hook passing in ref and root margin
8
// In this case it would only be considered onScreen if more ...
9
// ... than 300px of element is visible.
10
const onScreen = useOnScreen(ref, '-300px');
11
12
return (
13
<div>
14
<div style={{ height: '100vh' }}>
15
<h1>Scroll down to next section 👇</h1>
16
</div>
17
<div
18
ref={ref}
19
style={{
20
height: '100vh',
21
backgroundColor: onScreen ? '#23cebd' : '#efefef'
22
}}
23
>
24
{onScreen ? (
25
<div>
26
<h1>Hey I'm on the screen</h1>
27
<img src="https://i.giphy.com/media/ASd0Ukj0y3qMM/giphy.gif" />
28
</div>
29
) : (
30
<h1>Scroll down 300px from the top of this section 👇</h1>
31
)}
32
</div>
33
</div>
34
);
35
}
36
37
// Hook
38
function useOnScreen(ref, rootMargin = '0px') {
39
// State and setter for storing whether element is visible
40
const [isIntersecting, setIntersecting] = useState(false);
41
42
useEffect(() => {
43
const observer = new IntersectionObserver(
44
([entry]) => {
45
// Update our state when observer callback fires
46
setIntersecting(entry.isIntersecting);
47
},
48
{
49
rootMargin
50
}
51
);
52
if (ref.current) {
53
observer.observe(ref.current);
54
}
55
return () => {
56
observer.unobserve(ref.current);
57
};
58
}, []); // Empty array ensures that effect is only run on mount and unmount
59
60
return isIntersecting;
61
}
Copied!

usePrevious

1
import { useState, useEffect, useRef } from 'react';
2
3
// Usage
4
function App() {
5
// State value and setter for our example
6
const [count, setCount] = useState(0);
7
8
// Get the previous value (was passed into hook on last render)
9
const prevCount = usePrevious(count);
10
11
// Display both current and previous count value
12
return (
13
<div>
14
<h1>Now: {count}, before: {prevCount}</h1>
15
<button onClick={() => setCount(count + 1)}>Increment</button>
16
</div>
17
);
18
}
19
20
// Hook
21
function usePrevious(value) {
22
// The ref object is a generic container whose current property is mutable ...
23
// ... and can hold any value, similar to an instance property on a class
24
const ref = useRef();
25
26
// Store current value in ref
27
useEffect(() => {
28
ref.current = value;
29
}, [value]); // Only re-run if value changes
30
31
// Return previous value (happens before update in useEffect above)
32
return ref.current;
33
}
Copied!

useWindowSize

1
import { useState, useEffect } from 'react';
2
3
// Usage
4
function App() {
5
const size = useWindowSize();
6
7
return (
8
<div>
9
{size.width}px / {size.height}px
10
</div>
11
);
12
}
13
14
// Hook
15
function useWindowSize() {
16
// Initialize state with undefined width/height so server and client renders match
17
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
18
const [windowSize, setWindowSize] = useState({
19
width: undefined,
20
height: undefined,
21
});
22
23
useEffect(() => {
24
// Handler to call on window resize
25
function handleResize() {
26
// Set window width/height to state
27
setWindowSize({
28
width: window.innerWidth,
29
height: window.innerHeight,
30
});
31
}
32
33
// Add event listener
34
window.addEventListener("resize", handleResize);
35
36
// Call handler right away so state gets updated with initial window size
37
handleResize();
38
39
// Remove event listener on cleanup
40
return () => window.removeEventListener("resize", handleResize);
41
}, []); // Empty array ensures that effect is only run on mount
42
43
return windowSize;
44
}
Copied!

useHover

1
import { useRef, useState, useEffect } from 'react';
2
3
// Usage
4
function App() {
5
const [hoverRef, isHovered] = useHover();
6
7
return (
8
<div ref={hoverRef}>
9
{isHovered ? '😁' : '☹️'}
10
</div>
11
);
12
}
13
14
// Hook
15
function useHover() {
16
const [value, setValue] = useState(false);
17
18
const ref = useRef(null);
19
20
const handleMouseOver = () => setValue(true);
21
const handleMouseOut = () => setValue(false);
22
23
useEffect(
24
() => {
25
const node = ref.current;
26
if (node) {
27
node.addEventListener('mouseover', handleMouseOver);
28
node.addEventListener('mouseout', handleMouseOut);
29
30
return () => {
31
node.removeEventListener('mouseover', handleMouseOver);
32
node.removeEventListener('mouseout', handleMouseOut);
33
};
34
}
35
},
36
[ref.current] // Recall only if ref changes
37
);
38
39
return [ref, value];
40
}
Copied!

useAsync

1
const useAsync = fn => {
2
const initialState = { loading: false, error: null, value: null };
3
const stateReducer = (_, action) => {
4
switch (action.type) {
5
case 'start':
6
return { loading: true, error: null, value: null };
7
case 'finish':
8
return { loading: false, error: null, value: action.value };
9
case 'error':
10
return { loading: false, error: action.error, value: null };
11
}
12
};
13
14
const [state, dispatch] = React.useReducer(stateReducer, initialState);
15
16
const run = async (args = null) => {
17
try {
18
dispatch({ type: 'start' });
19
const value = await fn(args);
20
dispatch({ type: 'finish', value });
21
} catch (error) {
22
dispatch({ type: 'error', error });
23
}
24
};
25
26
return { ...state, run };
27
};
28
29
const RandomImage = props => {
30
const imgFetch = useAsync(url =>
31
fetch(url).then(response => response.json())
32
);
33
34
return (
35
<div>
36
<button
37
onClick={() => imgFetch.run('https://dog.ceo/api/breeds/image/random')}
38
disabled={imgFetch.isLoading}
39
>
40
Load image
41
</button>
42
<br />
43
{imgFetch.loading && <div>Loading...</div>}
44
{imgFetch.error && <div>Error {imgFetch.error}</div>}
45
{imgFetch.value && (
46
<img
47
src={imgFetch.value.message}
48
alt="avatar"
49
width={400}
50
height="auto"
51
/>
52
)}
53
</div>
54
);
55
};
56
57
ReactDOM.render(<RandomImage />, document.getElementById('root'));
Copied!

useComponentDidMount

1
const useComponentDidMount = onMountHandler => {
2
React.useEffect(() => {
3
onMountHandler();
4
}, []);
5
};
6
7
const Mounter = () => {
8
useComponentDidMount(() => console.log('Component did mount'));
9
10
return <div>Check the console!</div>;
11
};
12
13
ReactDOM.render(<Mounter />, document.getElementById('root'));
Copied!

useComponentWillUnmount

1
const useComponentWillUnmount = onUnmountHandler => {
2
React.useEffect(
3
() => () => {
4
onUnmountHandler();
5
},
6
[]
7
);
8
};
9
10
const Unmounter = () => {
11
useComponentWillUnmount(() => console.log('Component will unmount'));
12
13
return <div>Check the console!</div>;
14
};
15
16
ReactDOM.render(<Unmounter />, document.getElementById('root'));
Copied!

useInterval

1
const useInterval = (callback, delay) => {
2
const savedCallback = React.useRef();
3
4
React.useEffect(() => {
5
savedCallback.current = callback;
6
}, [callback]);
7
8
React.useEffect(() => {
9
function tick() {
10
savedCallback.current();
11
}
12
if (delay !== null) {
13
let id = setInterval(tick, delay);
14
return () => clearInterval(id);
15
}
16
}, [delay]);
17
};
18
19
const Timer = props => {
20
const [seconds, setSeconds] = React.useState(0);
21
useInterval(() => {
22
setSeconds(seconds + 1);
23
}, 1000);
24
25
return <p>{seconds}</p>;
26
};
27
28
ReactDOM.render(<Timer />, document.getElementById('root'));
Copied!

useTimeout

1
const useTimeout = (callback, delay) => {
2
const savedCallback = React.useRef();
3
4
React.useEffect(() => {
5
savedCallback.current = callback;
6
}, [callback]);
7
8
React.useEffect(() => {
9
function tick() {
10
savedCallback.current();
11
}
12
if (delay !== null) {
13
let id = setTimeout(tick, delay);
14
return () => clearTimeout(id);
15
}
16
}, [delay]);
17
};
18
19
const OneSecondTimer = props => {
20
const [seconds, setSeconds] = React.useState(0);
21
useTimeout(() => {
22
setSeconds(seconds + 1);
23
}, 1000);
24
25
return <p>{seconds}</p>;
26
};
27
28
ReactDOM.render(<OneSecondTimer />, document.getElementById('root'));
Copied!

useToggler

1
const useToggler = initialState => {
2
const [value, setValue] = React.useState(initialState);
3
4
const toggleValue = React.useCallback(() => setValue(prev => !prev), []);
5
6
return [value, toggleValue];
7
};
8
9
const Switch = () => {
10
const [val, toggleVal] = useToggler(false);
11
return <button onClick={toggleVal}>{val ? 'ON' : 'OFF'}</button>;
12
};
13
ReactDOM.render(<Switch />, document.getElementById('root'));
Copied!

useFetch

1
const useFetch = (url, options) => {
2
const [response, setResponse] = React.useState(null);
3
const [error, setError] = React.useState(null);
4
5
React.useEffect(() => {
6
const fetchData = async () => {
7
try {
8
const res = await fetch(url, options);
9
const json = await res.json();
10
setResponse(json);
11
} catch (error) {
12
setError(error);
13
}
14
};
15
fetchData();
16
}, []);
17
18
return { response, error };
19
};
20
21
const ImageFetch = props => {
22
const res = useFetch('https://dog.ceo/api/breeds/image/random', {});
23
if (!res.response) {
24
return <div>Loading...</div>;
25
}
26
const imageUrl = res.response.message;
27
return (
28
<div>
29
<img src={imageUrl} alt="avatar" width={400} height="auto" />
30
</div>
31
);
32
};
33
34
ReactDOM.render(<ImageFetch />, document.getElementById('root'));
Copied!

useNavigatorOnLine

1
const getOnLineStatus = () =>
2
typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean'
3
? navigator.onLine
4
: true;
5
6
const useNavigatorOnLine = () => {
7
const [status, setStatus] = React.useState(getOnLineStatus());
8
9
const setOnline = () => setStatus(true);
10
const setOffline = () => setStatus(false);
11
12
React.useEffect(() => {
13
window.addEventListener('online', setOnline);
14
window.addEventListener('offline', setOffline);
15
16
return () => {
17
window.removeEventListener('online', setOnline);
18
window.removeEventListener('offline', setOffline);
19
};
20
}, []);
21
22
return status;
23
};
24
25
const StatusIndicator = () => {
26
const isOnline = useNavigatorOnLine();
27
28
return <span>You are {isOnline ? 'online' : 'offline'}.</span>;
29
};
30
31
ReactDOM.render(<StatusIndicator />, document.getElementById('root'));
Copied!

useMediaQuery

1
const useMediaQuery = (query, whenTrue, whenFalse) => {
2
if (typeof window === 'undefined' || typeof window.matchMedia === 'undefined')
3
return whenFalse;
4
5
const mediaQuery = window.matchMedia(query);
6
const [match, setMatch] = React.useState(!!mediaQuery.matches);
7
8
React.useEffect(() => {
9
const handler = () => setMatch(!!mediaQuery.matches);
10
mediaQuery.addListener(handler);
11
return () => mediaQuery.removeListener(handler);
12
}, []);
13
14
return match ? whenTrue : whenFalse;
15
};
16
17
const ResponsiveText = () => {
18
const text = useMediaQuery(
19
'(max-width: 400px)',
20
'Less than 400px wide',
21
'More than 400px wide'
22
);
23
24
return <span>{text}</span>;
25
};
26
27
ReactDOM.render(<ResponsiveText />, document.getElementById('root'));
Copied!

useTouch

1
import React from 'react';
2
3
function useTouch(): [
4
boolean,
5
{
6
onTouchStart: (e: React.TouchEvent) => void;
7
onTouchEnd: (e: React.TouchEvent) => void;
8
}
9
] {
10
const [isTouched, setTouched] = React.useState(false);
11
12
const bind = React.useMemo(
13
() => ({
14
onTouchStart: (e: React.TouchEvent) => void setTouched(true),
15
onTouchEnd: (e: React.TouchEvent) => void setTouched(false),
16
}),
17
[]
18
);
19
20
return [isTouched, bind];
21
}
22
23
export default useTouch;
Copied!

useCookie

1
import { useState } from 'react';
2
3
const setCookie = (name, value, days, path) => {
4
const expires = new Date(Date.now() + days * 864e5).toUTCString();
5
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=${path}`;
6
};
7
8
const getCookie = (name) => document.cookie.split('; ').reduce((r, v) => {
9
const parts = v.split('=');
10
return parts[0] === name ? decodeURIComponent(parts[1]) : r;
11
}, '');
12
13
const useCookie = (cookieName, initialValue) => {
14
const [cookieValue, setCookieValue] = useState(() => getCookie(cookieName) || initialValue);
15
16
const updateCookie = (value, days = 365, path = '/') => {
17
setCookieValue(value);
18
setCookie(cookieName, value, days, path);
19
};
20
21
const deleteCookie = (path = '/') => {
22
updateCookie('', -1, path);
23
setCookieValue(null);
24
};
25
26
return [cookieValue, updateCookie, deleteCookie];
27
};
28
29
export default useCookie;
30
31
// Usage
32
33
// const App = () => {
34
// const [userToken, setUserToken, deleteUserToken] = useCookie('token', '0');
35
36
// render(
37
// <div>
38
// <p>{userToken}</p>
39
// <button onClick={() => setUserToken('123')}>Change token</button>
40
// <button onClick={() => deleteUserToken()}>Delete token</button>
41
// </div>
42
// );
43
// };
Copied!

useLogger

1
import { useEffect } from 'react';
2
3
export function useLogger(name: string, props: any): void {
4
useEffect(() => {
5
console.log(`${name} has mounted`);
6
return () => console.log(`${name} has unmounted`);
7
}, [name]);
8
useEffect(() => {
9
console.log(`${name} Props updated`, props);
10
});
11
}
12
13
export default useLogger;
Copied!

useTitle

1
import { useEffect } from "react";
2
3
const useTitle = title => {
4
useEffect(() => {
5
document.title = title;
6
}, [title]);
7
};
8
9
export default useTitle;
Copied!

useLifecycles

1
import { useEffect } from 'react';
2
3
const useLifecycles = (mount, unmount?) => {
4
useEffect(() => {
5
if (mount) {
6
mount();
7
}
8
return () => {
9
if (unmount) {
10
unmount();
11
}
12
};
13
}, []);
14
};
15
16
export default useLifecycles;
Copied!

useRendersCount

1
import { useRef } from 'react';
2
3
export function useRendersCount(): number {
4
return ++useRef(0).current;
5
}
Copied!

useUpdate

1
// 强制组件重新渲染的 hook。
2
import { useCallback, useState } from 'react';
3
4
const useUpdate = () => {
5
const [, setState] = useState({});
6
7
return useCallback(() => setState({}), []);
8
};
9
10
export default useUpdate;
Copied!