前言
当谈到现代前端开发和React时,React Hook 是一个不可或缺的话题。它们是函数式组件的利器,为开发者提供了更强大、更灵活的工具,让你的应用更易于维护和扩展。无论你是React的新手还是老手,优秀的React Hook都有助于你在项目中更高效地工作。本文将分享一些实用的React Hook,它们可以帮助你简化组件的逻辑,提高代码的可读性,以及增加应用的性能和响应性。
业界流行的React Hook库
首先列举下当前比较主流的React Hook库
其中ahooks算是高水准的Hooks库,而且笔者是ahooks的重度使用者,所以本次会从该库中抽取一些开发过程中常用的(带有笔者墙裂的主观色彩)Hooks进行介绍
Hooks介绍
Scene
useTextSelection
实时获取用户当前选取的文本内容及位置。
食用方法
jsx
import React from 'react';
import { useTextSelection } from 'ahooks';
export default () => {
const { text } = useTextSelection();
return (
<div>
<p>You can select text all page.</p>
<p>Result:{text}</p>
</div>
);
};
useVirtualList
提供虚拟化列表能力的 Hook,用于解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题。
食用方法
jsx
import React, { useMemo, useRef } from 'react';
import { useVirtualList } from 'ahooks';
export default () => {
const containerRef = useRef(null);
const wrapperRef = useRef(null);
const originalList = useMemo(() => Array.from(Array(99999).keys()), []);
const [list] = useVirtualList(originalList, {
containerTarget: containerRef,
wrapperTarget: wrapperRef,
itemHeight: 60,
overscan: 10,
});
return (
<>
<div ref={containerRef} style={{ height: '300px', overflow: 'auto', border: '1px solid' }}>
<div ref={wrapperRef}>
{list.map((ele) => (
<div
style={{
height: 52,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #e8e8e8',
marginBottom: 8,
}}
key={ele.index}
>
Row: {ele.data}
</div>
))}
</div>
</div>
</>
);
};
笔者注: 实际上就是通过容器的高度、元素的高度和当前滚动条位置来计算当前应该显示哪一批数据。
LifeCycle
useMount
只在组件初始化时执行的 Hook
食用方法
jsx
import { useMount, useBoolean } from 'ahooks';
import { message } from 'antd';
import React from 'react';
const MyComponent = () => {
useMount(() => {
message.info('mount');
});
return <div>Hello World</div>;
};
export default () => {
const [state, { toggle }] = useBoolean(false);
return (
<>
<button type="button" onClick={toggle}>
{state ? 'unmount' : 'mount'}
</button>
{state && <MyComponent />}
</>
);
};
笔者注: 相当于空依赖的useEffect
jsx
import React from 'react'
export default function component() {
useEffect(() => {
// do something
}, [])
return (
<div>component</div>
)
}
useUnmount
在组件卸载(unmount)时执行的 Hook。
食用方法
jsx
import { useBoolean, useUnmount } from 'ahooks';
import { message } from 'antd';
import React from 'react';
const MyComponent = () => {
useUnmount(() => {
message.info('unmount');
});
return <p>Hello World!</p>;
};
export default () => {
const [state, { toggle }] = useBoolean(true);
return (
<>
<button type="button" onClick={toggle}>
{state ? 'unmount' : 'mount'}
</button>
{state && <MyComponent />}
</>
);
};
笔者注: 相当于空依赖的useEffect的返回函数
jsx
import React from 'react'
export default function component() {
useEffect(() => {
return () => {
// do something
}
}, [])
return (
<div>component</div>
)
}
State
useDebounce
用来处理防抖值的 Hook。
食用方法
jsx
import React, { useState } from 'react';
import { useDebounce } from 'ahooks';
export default () => {
const [value, setValue] = useState<string>();
const debouncedValue = useDebounce(value, { wait: 500 });
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Typed value"
style={{ width: 280 }}
/>
<p style={{ marginTop: 16 }}>DebouncedValue: {debouncedValue}</p>
</div>
);
};
笔者注: 对useState的value做防抖处理,并导出一个新的变量。这样就不会因为value频繁变化导致页面频繁渲染。
useThrottle
用来处理节流值的 Hook。
食用方法
jsx
import React, { useState } from 'react';
import { useThrottle } from 'ahooks';
export default () => {
const [value, setValue] = useState<string>();
const throttledValue = useThrottle(value, { wait: 500 });
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Typed value"
style={{ width: 280 }}
/>
<p style={{ marginTop: 16 }}>throttledValue: {throttledValue}</p>
</div>
);
};
笔者注: 道理同上,只是做了节流处理并导出一个新的变量。
useMap
管理 Map 类型状态的 Hook。
食用方法
jsx
import React from 'react';
import { useMap } from 'ahooks';
export default () => {
const [map, { set, setAll, remove, reset, get }] = useMap<string | number, string>([
['msg', 'hello world'],
[123, 'number type'],
]);
return (
<div>
<button type="button" onClick={() => set(String(Date.now()), new Date().toJSON())}>
Add
</button>
<button
type="button"
onClick={() => setAll([['text', 'this is a new Map']])}
style={{ margin: '0 8px' }}
>
Set new Map
</button>
<button type="button" onClick={() => remove('msg')} disabled={!get('msg')}>
Remove 'msg'
</button>
<button type="button" onClick={() => reset()} style={{ margin: '0 8px' }}>
Reset
</button>
<div style={{ marginTop: 16 }}>
<pre>{JSON.stringify(Array.from(map), null, 2)}</pre>
</div>
</div>
);
};
笔者注: 笔者用的最频繁的Hook之一。尤其来管理不定状态(数量不定、key不定)有奇效。
usePrevious
保存上一次状态的 Hook。
食用方法
jsx
import { usePrevious } from 'ahooks';
import React, { useState } from 'react';
export default () => {
const [count, setCount] = useState(0);
const previous = usePrevious(count);
return (
<>
<div>counter current value: {count}</div>
<div style={{ marginBottom: 8 }}>counter previous value: {previous}</div>
<button type="button" onClick={() => setCount((c) => c + 1)}>
increase
</button>
<button type="button" style={{ marginLeft: 8 }} onClick={() => setCount((c) => c - 1)}>
decrease
</button>
</>
);
};
笔者注: 没啥好说的,就是记录当前状态的前一次数据。如果有对比新老数据的需求,有奇效。
useRafState
只在 requestAnimationFrame callback 时更新 state,一般用于性能优化。
食用方法
jsx
import { useRafState } from 'ahooks';
import React, { useEffect } from 'react';
export default () => {
const [state, setState] = useRafState({
width: 0,
height: 0,
});
useEffect(() => {
const onResize = () => {
setState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
});
};
onResize();
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}, []);
return (
<div>
<p>Try to resize the window </p>
current: {JSON.stringify(state)}
</div>
);
};
useSafeState
用法与
React.useState
完全一样,但是在组件卸载后异步回调内的setState
不再执行,避免因组件卸载后更新状态而导致的内存泄漏。
食用方法
jsx
import { useSafeState } from 'ahooks';
import React, { useEffect, useState } from 'react';
const Child = () => {
const [value, setValue] = useSafeState<string>();
useEffect(() => {
setTimeout(() => {
setValue('data loaded from server');
}, 5000);
}, []);
const text = value || 'Loading...';
return <div>{text}</div>;
};
export default () => {
const [visible, setVisible] = useState(true);
return (
<div>
<button onClick={() => setVisible(false)}>Unmount</button>
{visible && <Child />}
</div>
);
};
笔者注: 笔者最喜欢的Hook,没有之一。主要用来处理异步setState前就已经把组件卸载导致内存泄露的问题。如果你的控制台出现以下警告,那么这个Hook包治各种不服!
useGetState
给
React.useState
增加了一个 getter 方法,以获取当前最新值。
食用方法
jsx
import React, { useEffect } from 'react';
import { useGetState } from 'ahooks';
export default () => {
const [count, setCount, getCount] = useGetState<number>(0);
useEffect(() => {
const interval = setInterval(() => {
console.log('interval count', getCount());
}, 3000);
return () => {
clearInterval(interval);
};
}, []);
return <button onClick={() => setCount((count) => count + 1)}>count: {count}</button>;
};
笔者注: 在setInterval或者setTimeout等闭包场景下有奇效。
Effect
useUpdateEffect
useUpdateEffect
用法等同于useEffect
,但是会忽略首次执行,只在依赖更新时执行。
食用方法
jsx
import React, { useEffect, useState } from 'react';
import { useUpdateEffect } from 'ahooks';
export default () => {
const [count, setCount] = useState(0);
const [effectCount, setEffectCount] = useState(0);
const [updateEffectCount, setUpdateEffectCount] = useState(0);
useEffect(() => {
setEffectCount((c) => c + 1);
}, [count]);
useUpdateEffect(() => {
setUpdateEffectCount((c) => c + 1);
return () => {
// do something
};
}, [count]); // you can include deps array if necessary
return (
<div>
<p>effectCount: {effectCount}</p>
<p>updateEffectCount: {updateEffectCount}</p>
<p>
<button type="button" onClick={() => setCount((c) => c + 1)}>
reRender
</button>
</p>
</div>
);
};
笔者注: 如果你在组件中有个依赖某个变量而执行的逻辑且不希望该逻辑在组件初始化时候执行的话,那么这个Hook有奇效
useDebounceEffect
为
useEffect
增加防抖的能力。
食用方法
jsx
import { useDebounceEffect } from 'ahooks';
import React, { useState } from 'react';
export default () => {
const [value, setValue] = useState('hello');
const [records, setRecords] = useState<string[]>([]);
useDebounceEffect(
() => {
setRecords((val) => [...val, value]);
},
[value],
{
wait: 1000,
},
);
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Typed value"
style={{ width: 280 }}
/>
<p style={{ marginTop: 16 }}>
<ul>
{records.map((record, index) => (
<li key={index}>{record}</li>
))}
</ul>
</p>
</div>
);
};
笔者注: 如果不希望依赖状态(频繁变化)来执行的逻辑不要那么频繁执行,那么就用这个吧
useDebounceFn
用来处理防抖函数的 Hook。
食用方法
jsx
import { useDebounceFn } from 'ahooks';
import React, { useState } from 'react';
export default () => {
const [value, setValue] = useState(0);
const { run } = useDebounceFn(
() => {
setValue(value + 1);
},
{
wait: 500,
},
);
return (
<div>
<p style={{ marginTop: 16 }}> Clicked count: {value} </p>
<button type="button" onClick={run}>
Click fast!
</button>
</div>
);
};
笔者注: 没啥可说的,就是防!抖!
useThrottleEffect
为
useEffect
增加节流的能力。
食用方法
jsx
import React, { useState } from 'react';
import { useThrottleEffect } from 'ahooks';
export default () => {
const [value, setValue] = useState('hello');
const [records, setRecords] = useState<string[]>([]);
useThrottleEffect(
() => {
setRecords((val) => [...val, value]);
},
[value],
{
wait: 1000,
},
);
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Typed value"
style={{ width: 280 }}
/>
<p style={{ marginTop: 16 }}>
<ul>
{records.map((record, index) => (
<li key={index}>{record}</li>
))}
</ul>
</p>
</div>
);
};
笔者注: 如果不希望依赖状态(频繁变化)来执行的逻辑按某个节奏来执行,那么就用这个吧
useThrottleFn
用来处理函数节流的 Hook。
食用方法
jsx
import React, { useState } from 'react';
import { useThrottleFn } from 'ahooks';
export default () => {
const [value, setValue] = useState(0);
const { run } = useThrottleFn(
() => {
setValue(value + 1);
},
{ wait: 500 },
);
return (
<div>
<p style={{ marginTop: 16 }}> Clicked count: {value} </p>
<button type="button" onClick={run}>
Click fast!
</button>
</div>
);
};
笔者注: 没啥可说的,就是节!流!
useRafInterval
用
requestAnimationFrame
模拟实现setInterval
,API 和useInterval
保持一致,好处是可以在页面不渲染的时候停止执行定时器,比如页面隐藏或最小化等。
请注意,如下两种情况下很可能是不适用的,优先考虑useInterval
:
- 时间间隔小于
16ms
- 希望页面不渲染的情况下依然执行定时器
Node 环境下 requestAnimationFrame
会自动降级到 setInterval
食用方法
jsx
import React, { useState } from 'react';
import { useRafInterval } from 'ahooks';
export default () => {
const [count, setCount] = useState(0);
useRafInterval(() => {
setCount(count + 1);
}, 1000);
return <div>count: {count}</div>;
};
笔者注: 性能优化有他没毛病!
useUpdate
useUpdate 会返回一个函数,调用该函数会强制组件重新渲染。
食用方法
jsx
import React from 'react';
import { useUpdate } from 'ahooks';
export default () => {
const update = useUpdate();
return (
<>
<div>Time: {Date.now()}</div>
<button type="button" onClick={update} style={{ marginTop: 8 }}>
update
</button>
</>
);
};
笔者注: 手动触发组件重渲染,关键时候大用处。
Dom
useEventListener
优雅的使用 addEventListener。
食用方法
jsx
import React, { useState, useRef } from 'react';
import { useEventListener } from 'ahooks';
export default () => {
const [value, setValue] = useState(0);
const ref = useRef(null);
useEventListener(
'click',
() => {
setValue(value + 1);
},
{ target: ref },
);
return (
<button ref={ref} type="button">
You click {value} times
</button>
);
};
笔者注: 不用手动
removeEventListener
,好用就对了!
useSize
监听 DOM 节点尺寸变化的 Hook。
食用方法
jsx
import React, { useRef } from 'react';
import { useSize } from 'ahooks';
export default () => {
const ref = useRef(null);
const size = useSize(ref);
return (
<div ref={ref}>
<p>Try to resize the preview window </p>
<p>
width: {size?.width}px, height: {size?.height}px
</p>
</div>
);
};
笔者注: 监听节点
resize
就用他,一行代码解决的事情就不要折腾了!
useClickAway
监听目标元素外的点击事件。
食用方法
jsx
import React, { useState, useRef } from 'react';
import { useClickAway } from 'ahooks';
export default () => {
const [counter, setCounter] = useState(0);
const ref = useRef<HTMLButtonElement>(null);
useClickAway(() => {
setCounter((s) => s + 1);
}, ref);
return (
<div>
<button ref={ref} type="button">
box
</button>
<p>counter: {counter}</p>
</div>
);
};
useDocumentVisibility
监听页面是否可见,参考 visibilityState API
食用方法
jsx
import React, { useEffect } from 'react';
import { useDocumentVisibility } from 'ahooks';
export default () => {
const documentVisibility = useDocumentVisibility();
useEffect(() => {
console.log(`Current document visibility state: ${documentVisibility}`);
}, [documentVisibility]);
return <div>Current document visibility state: {documentVisibility}</div>;
};
笔者注: 一行代码解决的事情就不要折腾了!一行代码解决的事情就不要折腾了!
useInViewport
观察元素是否在可见区域,以及元素可见比例。更多信息参考 Intersection Observer API
食用方法
jsx
import React, { useRef } from 'react';
import { useInViewport } from 'ahooks';
export default () => {
const ref = useRef(null);
const [inViewport] = useInViewport(ref);
return (
<div>
<div style={{ width: 300, height: 300, overflow: 'scroll', border: '1px solid' }}>
scroll here
<div style={{ height: 800 }}>
<div
ref={ref}
style={{
border: '1px solid',
height: 100,
width: 100,
textAlign: 'center',
marginTop: 80,
}}
>
observer dom
</div>
</div>
</div>
<div style={{ marginTop: 16, color: inViewport ? '#87d068' : '#f50' }}>
inViewport: {inViewport ? 'visible' : 'hidden'}
</div>
</div>
);
};
笔者注: 一行代码解决的事情就不要折腾了!一行代码解决的事情就不要折腾了!一行代码解决的事情就不要折腾了!
Advanced
useLatest
返回当前最新值的 Hook,可以避免闭包问题。
食用方法
jsx
import React, { useState, useEffect } from 'react';
import { useLatest } from 'ahooks';
export default () => {
const [count, setCount] = useState(0);
const latestCountRef = useLatest(count);
useEffect(() => {
const interval = setInterval(() => {
setCount(latestCountRef.current + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<>
<p>count: {count}</p>
</>
);
};
笔者注: 与
useGetState
有异曲同工之妙
useMemoizedFn
持久化 function 的 Hook,理论上,可以使用 useMemoizedFn 完全代替 useCallback。
在某些场景中,我们需要使用 useCallback 来记住一个函数,但是在第二个参数 deps 变化时,会重新生成函数,导致函数地址变化。
jsx
const [state, setState] = useState('');
// 在 state 变化时,func 地址会变化
const func = useCallback(() => {
console.log(state);
}, [state]);
使用 useMemoizedFn,可以省略第二个参数 deps,同时保证函数地址永远不会变化。
食用方法
jsx
const [state, setState] = useState('');
// func 地址永远不会变化
const func = useMemoizedFn(() => {
console.log(state);
});
笔者注: 将一个回调函数通过props传入子组件且不希望因为函数地址发生变化导致组件无效渲染,那么就用他吧。
Dev
useTrackedEffect
追踪是哪个依赖变化触发了
useEffect
的执行。
食用方法
jsx
import React, { useState } from 'react';
import { useTrackedEffect } from 'ahooks';
export default () => {
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);
useTrackedEffect(
(changes) => {
console.log('Index of changed dependencies: ', changes);
},
[count, count2],
);
return (
<div>
<p>Please open the browser console to view the output!</p>
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>count + 1</button>
</div>
<div style={{ marginTop: 16 }}>
<p>Count2: {count2}</p>
<button onClick={() => setCount2((c) => c + 1)}>count + 1</button>
</div>
</div>
);
};
笔者注: 调试利器!
useWhyDidYouUpdate
帮助开发者排查是哪个属性改变导致了组件的 rerender。
食用方法
jsx
import { useWhyDidYouUpdate } from 'ahooks';
import React, { useState } from 'react';
const Demo: React.FC<{ count: number }> = (props) => {
const [randomNum, setRandomNum] = useState(Math.random());
useWhyDidYouUpdate('useWhyDidYouUpdateComponent', { ...props, randomNum });
return (
<div>
<div>
<span>number: {props.count}</span>
</div>
<div>
randomNum: {randomNum}
<button onClick={() => setRandomNum(Math.random)} style={{ marginLeft: 8 }}>
🎲
</button>
</div>
</div>
);
};
export default () => {
const [count, setCount] = useState(0);
return (
<div>
<Demo count={count} />
<div>
<button onClick={() => setCount((prevCount) => prevCount - 1)}>count -</button>
<button onClick={() => setCount((prevCount) => prevCount + 1)} style={{ marginLeft: 8 }}>
count +
</button>
</div>
<p style={{ marginTop: 8 }}>Please open the browser console to view the output!</p>
</div>
);
};
笔者注: 想知道组件为啥频繁渲染?用这个Hook试下?