🧭 使用指引
@shined/react-use
旨在引导一种重塑 React 开发的新编程范式。它通过提供众多高质量、语意化的 Hooks 来帮助开发者提高开发效率、养成更好的编程习惯,减少对 useEffect
和 useState
等的直接依赖,同时期望开发者能够逐渐适应「Hooks 优先」的 React 开发(编程)范式。
以下是一些常见场景的使用指引,以帮助你更好地使用 @shined/react-use
。
提示
以下仅包含部分常见场景的使用指引,遵循最佳实践,具体使用请根据实际情况选择。
替换 useState
useState
是 React 中用来管理组件状态的 Hook,基本上每个 React 开发者都会用到。
但是低版本 React 中的 useState
可能导致非预期的行为。比如在 React <= 17 时,当组件卸载后,调用 setState
会抛出令人困惑的警告(参考 安全状态)。此外,React 内部使用浅比较来判断状态是否改变,这可能会导致组件进行不必要的重渲染,从而影响性能。
useSafeState
useSafeState
被设计为 useState
的直接替代方案,用于规避低版本 React 下的警告问题,并遵循官方做法在高版本 React 下与 useState
行为保持一致。
同时它还具备可选的性能优化特性(deep
选项,深度比较状态,确认变更再更新,默认 false
)。
更多详情请参考 安全状态 和 useSafeState。
const [name, setName] = useState('react')
// 替换为
const [name, setName] = useSafeState('react')
const [state, setState] = useState({ count: 0 })
setState({ count: 0 }) // 触发重新渲染
setState({ count: 0 }) // 触发重新渲染
setState({ count: 0 }) // 触发重新渲染
// 替换为
const [state, setState] = useSafeState({ count: 0 }, { deep: true })
setState({ count: 0 }) // 不会触发重新渲染
setState({ count: 0 }) // 不会触发重新渲染
setState({ count: 0 }) // 不会触发重新渲染
// deep 为可选项,当状态简单、可控,且状态值的地址频繁变动,但实际值未改变时,将显著降低渲染次数
useBoolean
useBoolean
用于管理布尔值状态,提供了一系列语意化的操作函数,例如 toggle
、setTrue
、setFalse
等,底层使用 useSafeState
以确保状态安全。
详情参考 useBoolean。
const [bool, actions] = useBoolean(false)
actions.toggle() // true
actions.setTrue() // true
actions.setFalse() // false
useCounter
useCounter
用于管理 number 类型状态,提供了一系列语意化的操作函数,例如 inc
、dec
、set
等,底层使用 useSafeState
以确保状态安全。
详情参考 useCounter。
const [count, actions] = useCounter(0)
actions.inc() // 1
actions.inc(10) // 11
actions.dec() // 10
actions.set(20) // 20
减少 useEffect
useEffect
是 React 中最基础、最常用的 Hook 之一,但一般情况下,我们并不推荐直接使用。因为它的使用方式相对较为原始,且容易出现副作用难以控制,或副作用与预期不符等问题。
@shined/react-use
提供了一系列高质量、语意化的 Hooks 来等价替换部分 useEffect
调用场景。
useMount
我们可能会这样使用 useEffect
,功能上等同于组件挂载时执行一次 doSomething()
。
// 不推荐
useEffect(() => {
doSomething()
}, [])
或者,当我们需要在挂载时执行一些异步操作且需要拿到结时果,我们通常会包一层 async 函数来执行异步操作。
// 不推荐
useEffect(() => {
async function asyncWrapper() {
const result = await doSomethingAsync()
const.log(result)
}
asyncWrapper()
}, [])
以上代码在逻辑上完全没问题,但是存在代码可读性差、缺乏语意化、后期难以维护、可能意外返回清理函数等诸多问题和隐患,同时对异步函数支持不够友好。推荐替换为更加语义化的 useMount
,支持异步函数。
详情参考 useMount。
// 推荐
useMount(doSomething)
// 推荐
useMount(async () => {
const result = await doSomethingAsync()
const.log(result)
})
useUnmount
useUnmount
用于在组件卸载时执行一些操作,例如清理副作用,基本与 useMount
类似,但执行时机不同。
详情参考 useUnmount。
// 不推荐
useEffect(() => {
return () => {
doSomething()
}
}, [])
// 推荐
useUnmount(doSomething)
useUpdateEffect
useUpdateEffect
用于在组件更新时执行一些操作,例如监听某些状态的变化并执行操作,但是忽略首次渲染,适用于不需要立即执行副作用的场景。
详情参考 useUpdateEffect。
// 不推荐
const isMount = useRef(false)
useEffect(() => {
if (isMount.current) {
doSomething()
} else {
isMount.current = true
}
}, [state])
// 推荐
useUpdateEffect(() => {
doSomething()
}, [state])
useEffectOnce
useEffectOnce
用于在组件挂载时执行一次操作,在组件卸载时也执行一次操作,适用于只需要执行一次副作用的场景,本质上 useEffectOnce
是 useMount
和 useUnmount
的组合。
详情参考 useEffectOnce。
// 不推荐
useEffect(() => {
doSomething()
return () => clearSomething()
}, [])
// 推荐
useEffectOnce(() => {
doSomething()
return () => clearSomething()
})
useAsyncEffect
useAsyncEffect
用于在状态变更时执行异步操作,适用于需要监听状态变化并执行异步操作的场景。
详情参考 useAsyncEffect。
// 不推荐
useEffect(() => {
async function asyncWrapper() {
const result = await doSomethingAsync()
// 当前 Effect 执行结束后,可能仍然执行后续逻辑,存在内存泄漏等安全风险
doSomethingAfter(result)
}
asyncWrapper()
}, [state])
// 推荐
useAsyncEffect(async (isCancelled) => {
const result = await doSomethingAsync()
if(isCancelled()) {
// 如果当前 Effect 执行结束,不会执行后续逻辑
clearSomething()
return
}
doSomethingAfter(result)
}, [state])
常见场景
异步操作
推荐使用 useAsyncFn
来处理异步操作,内部自动处理 loading 等状态,拥有完善的生命周期。
详情参考 useAsyncFn。
// 不推荐
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const handleClick = async (post) => {
setLoading(true)
try {
await createPost(post)
} catch (error) {
setError(error)
console.error(error)
} finally {
setLoading(false)
}
}
return (
<button disabled={loading} onClick={() => handleClick({ title: 'Hello' })}>
Create Post
</button>
)
// 推荐
const { run, loading, error } = useAsyncFn(createPost, { onError: (error) => console.error(error) })
return (
<button disabled={loading} onClick={() => run({ title: 'Hello' })}>
Create Post
</button>
)
数据请求
推荐使用 useQuery
来处理数据请求,它提供了一系列语意化的操作函数,例如 run
、cancel
、refresh
等,同时支持自动处理 loading、error、data 等状态。
详情参考 useQuery。
// 不推荐
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [data, setData] = useState(null)
useEffect(() => {
setLoading(true)
fetch('https://api.example.com/data')
.then((res) => res.json())
.then((data) => {
setData(data)
setLoading(false)
})
.catch((error) => {
setError(error)
setLoading(false)
})
}, [])
// 推荐
const fetcher = () => fetch('https://api.example.com/data').then((res) => res.json())
const { loading, error, data, run, cancel, refresh } = useQuery(fetcher, {
refreshInterval: 1000
});
// 可以通过 run、cancel、refresh 等操作函数来控制数据请求
run()
防抖和节流
推荐使用 useDebouncedFn
和 useThrottledFn
两个 Hook 来处理常见的防抖和节流功能,当然也有 useDebouncedEffect 和 useDebouncedEffect 两个 Hook,用于处理防抖和节流的副作用,但一般情况下,我们更推荐前者。
详情参考 useDebouncedFn 和 useThrottledFn。
const handleSubmit = (value) => console.log(value)
const debouncedHandleSubmit = useDebouncedFn(handleSubmit, 500)
const handleScroll = (event) => console.log('scroll')
const throttledHandleScroll = useThrottledFn(handleScroll, 500)
推荐使用 usePagination
来处理分页逻辑,它提供了一系列语意化的操作函数,例如 next
、prev
、go
等,同时支持自定义页码、每页条数、总条数等。
详情参考 usePagination。
// 不推荐
const [total, setTotal] = useState(100)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const pageCount = Math.ceil(total / pageSize)
const handlePageChange = (page) => {
console.log(page, pageSize, pageCount, total)
}
useUpdateEffect(() => {
handlePageChange(page)
}, [page])
// 推荐
const [state, actions] = usePagination({
total: 100,
pageSize: 10,
onPageChange: (state) => {
console.log(state.page, state.pageSize, state.pageCount, state.total)
}
})
// state 包含了各种分页状态
const { page, pageSize, pageCount, total, isFirstPage, isLastPage } = state
// actions 可以进行各种分页操作
actions.next() // 下一页
actions.prev() // 上一页
actions.go(3) // 跳转到第 3 页
actions.setPageSize(20) // 设置每页条数为 20
复制到剪贴板
useClipboard
用于复制文本到剪贴板,适用于需要复制文本到剪贴板的场景,默认情况下使用 Clipboard API,如果浏览器不支持,则自动优雅降级到 document.execCommand('copy')
。
详情参考 useClipboard。
// 不推荐
const copyToClipboard = () => {
const input = document.createElement('input')
document.body.appendChild(input)
input.value = 'Hello, React'
input.select()
document.execCommand('copy')
document.body.removeChild(input)
}
// 不推荐,引入了额外依赖,使用体验割裂
import copy from 'copy-to-clipboard'
import CopyToClipboard from 'react-copy-to-clipboard'
// 推荐
const clipboard = useClipboard()
clipboard.copy('Hello, React')
时间格式化
useDateFormat
用于格式化时间,提供了一种额外的轻量、灵活、使用体验统一的格式化方案选择,适用于需要格式化时间的场景,支持 unicode 标准格式化 token。
详情参考 useDateFormat。
// 引入了额外依赖,使用体验割裂
import dayjs from 'dayjs' // 使用约定式格式化 tokens
dayjs('2024/09/01').format('YYYY-MM-DD HH:mm:ss')
// 引入了额外依赖,使用体验割裂
import moment from 'moment' // 使用约定式格式化 tokens
moment('2024/09/01').format('YYYY-MM-DD HH:mm:ss')
// 引入了额外依赖,使用体验割裂
import dateFns from 'date-fns' // date-fns v2 开始使用 unicode 标准的格式化 tokens
dateFns.format(new Date(), 'yyyy-MM-dd HH:mm:ss')
// 推荐
// 默认使用约定式的格式化 tokens
const time = useDateFormat('2024/09/01', 'YYYY-MM-DD HH:mm:ss')
const time = useDateFormat(1724315857591, 'YYYY-MM-DD HH:mm:ss')
// 同时支持 Unicode 标准的格式化 tokens
const time = useDateFormat(new Date(), 'yyyy-MM-dd HH:mm:ss', { unicodeSymbols: true })
定时器
日常开发中经常使用 setTimeout
和 setInterval
来处理定时任务,直接使用相对繁琐,且要求开发者手动清理定时器,容易出现忘记清理、清理不及时等问题。
// 不推荐
useEffect(() => {
const timer = setTimeout(() => {
doSomething()
}, 1000)
return () => clearTimeout(timer)
}, [])
@shined/react-use
提供了 useTimeoutFn
和 useIntervalFn
两个 Hook 来处理定时任务,自动清理定时器,避免出现忘记清理、清理不及时等问题。
详情参考 useTimeoutFn 和 useIntervalFn。
// 推荐
useTimeoutFn(doSomething, 1000, { immediate: true })
useIntervalFn(doSomething, 1000, { immediate: true })
处理事件
useEventListener
用于在组件挂载时添加事件监听器,组件卸载时自动移除事件监听器,适用于需要添加事件监听器的场景。任何实现了 EventTarget
接口的对象都可以作为第一个参数传入,例如 window
、document
、ref.current
等。
// 符合以下接口的对象都可以作为第一个参数传入,SSR 下支持 `() => window` 的写法
export interface InferEventTarget<Events> {
addEventListener: (event: Events, fn?: any, options?: any) => any
removeEventListener: (event: Events, fn?: any, options?: any) => any
}
详情参考 useEventListener。
// 不推荐
useEffect(() => {
const handler = () => doSomething()
window.addEventListener('resize', handler, { passive: true })
return () => {
window.removeEventListener('resize', handler)
}
}, [])
// 推荐,且 SSR 友好
useEventListener('resize', doSomething, { passive: true })
// useEventListener(() => window, 'resize', doSomething, { passive: true })
浏览器 API
我们经常需要调用浏览器 API 来实现一些功能,包括但远不限于:
直接操作 API 可能会让代码变得复杂、难以维护,由于 API 的兼容性问题,开发者在处理这些问题时不仅增加了识别兼容情况的心智负担,还可能需要在识别后,增加代码复杂度以实现兼容(如使用历史遗留的 API 实现尽可能兼容)。此外,还需注意许多细节以适应 React 组件化开发。
例如,Fullscreen API 在不同浏览器及其版本中的实现有所不同,Battery Status API 只在部分浏览器中得到支持,而 EyeDropper API 目前仅在最新的 Chrome 和 Edge 浏览器中可用。
幸运的是,@shined/react-use
已经封装了许多常用的浏览器 API 以提供更好的使用体验,同时许多浏览器 API 相关的 Hooks 内部使用了 useSupported 统一返回了 API 的支持情况,使得开发者可以更加方便地使用浏览器 API。
想了解更多可用的浏览器 API Hooks,请访问 Hooks 列表页 的 Browser 分类。
SSR 相关
@shined/react-use
旨在提供更好的服务端渲染支持,所有 Hooks 都兼容服务端渲染,且不会产生副作用。
useIsomorphicLayoutEffect
useIsomorphicLayoutEffect
在服务端渲染时使用 useLayoutEffect
,在客户端渲染时使用 useEffect
,适用于需要在服务端渲染时执行同步副作用的场景。
详情参考 useIsomorphicLayoutEffect。
// 不推荐,SSR 时会抛出警告
useLayoutEffect(() => {
doSomething()
}, [state])
// 推荐,在运行时自动决定使用 `useLayoutEffect` 或 `useEffect`
useIsomorphicLayoutEffect(() => {
doSomething()
}, [state])
useCallback 与 useMemo
useCallback
和 useMemo
是 React 中用于性能优化的 Hook,常用来缓存函数和值,避免不必要的重复计算。
实际开发中我们除了性能优化外,可能还需要确保引用稳定性以避免不必要的副作用等问题。但是根据官方文档,useCallback
和 useMemo
仅供用于性能优化,并不保证引用稳定,因此在某些场景下并不符合需求。
针对上述情况,useStableFn
和 useCreation
在提供性能优化的同时,还确保了引用稳定性。
useStableFn
useStableFn
用于确保函数引用稳定,适用于需要确保函数引用稳定的场景,例如传递给子组件的回调函数。
详情参考 useStableFn。
// 不推荐
const handleClick = () => {
console.log('click')
}
return <HeavyComponent onClick={handleClick} />
// 推荐
const handleClick = useStableFn(() => {
console.log('click')
})
return <HeavyComponent onClick={handleClick} />
useCreation
useCreation
用于初始化操作,适用于需要确保初始化操作只执行一次的场景,例如初始化复杂对象、耗时操作等,useCreation
除了性能优化外,还能确保结果在不同渲染周期间保持引用稳定。
详情参考 useCreation。
// 不推荐
const heavyResult = useMemo(() => doHeavyWorkToInit(), [])
const dynamicResult = useMemo(() => doHeavyWorkToCreate(), [dependency])
// 推荐
const heavyResult = useCreation(() => doHeavyWorkToInit())
const dynamicResult = useCreation(() => doHeavyWorkToCreate(), [dependency])
进阶指引
如果你需要封装自定义 Hook,或者需要更多高级功能,可以参考以下进阶指引。
useStableFn
useStableFn
用于确保函数引用稳定,适用于需要确保函数引用稳定的场景,例如传递给子组件的回调函数,通常 Hooks 暴露的函数最好使用 useStableFn
包裹。
详情参考 useStableFn。
const fn = useStableFn(() => {
console.log('click')
})
useLatest
useLatest
用于获取最新的值,适用于需要获取最新值的场景,例如在异步操作中获取最新的状态,通常搭配 useStableFn
使用,以实现缓存稳定的回调函数同时获取最新状态。
详情参考 useLatest。
const latest = useLatest(value)
useTargetElement
useTargetElement
用于获取目标元素,适用于需要获取目标元素的场景,例如在自定义 Hook 中获取目标元素,确保使用体验的一致性。
详情参考 useTargetElement 和 ElementTarget。
const ref = useRef<HTMLDivElement>(null) // <div ref={ref} />
const targetRef = useTargetElement(ref)
const targetRef = useTargetElement('#my-div')
const targetRef = useTargetElement('#my-div .container')
const targetRef = useTargetElement(() => window)
const targetRef = useTargetElement(() => document.getElementById('my-div'))
// 不推荐,会引起 SSR 问题
const targetRef = useTargetElement(window)
// 不推荐,会引起 SSR 问题
const targetRef = useTargetElement(document.getElementById('my-div'))
useCreation
useCreation
用于初始化操作,适用于需要确保初始化操作只执行一次的场景,例如初始化复杂对象、耗时操作等,useCreation
除了性能优化外,还能确保结果在不同渲染周期间保持引用稳定。
详情参考 useCreation。
const initResult = useCreation(() => doHeavyWorkToInit())
useSupported
useSupported
用于获取浏览器 API 的支持情况,适用于需要判断浏览器 API 支持情况的场景。
详情参考 useSupported。
const isSupported = useSupported(() => 'geolocation' in navigator)
usePausable
usePausable
用于创建一个 Pausable 实例,以赋予 Hooks 可暂停的能力,适用于需要暂停和恢复的场景。
详情参考 usePausable。
const pausable = usePausable(false, pauseCallback, resumeCallback)
useGetterRef
useGetterRef
暴露了一个函数以获取 ref.current
的最新值,适用于需要存储状态但不想触发重新渲染,同时需要获取最新值的场景。
详情参考 useGetterRef。
const [isActive, isActiveRef] = useGetterRef(false)