只会用 useEffect 和 useState?您有份代码质量提升指南请查收。

查看 7|回复 0
作者:VikiQAQ   
在 React 世界里,useEffect 和 useState 无疑是最常见的钩子( Hooks ),它们让函数组件拥有了类组件的力量。useState 赋予了组件维护状态的能力,而 useEffect 则让副作用管理变得简单。
对许多开发者来说,上述两个 Hooks 是 React 钩子的入门级应用,但随着项目复杂度的提升,仅仅使用这两个钩子可能会让组件变得臃肿,逻辑复杂难以维护。此时,众多开源社区的 React Hooks 库应运而生,它们提供了许多高质量、语义化的 Hooks ,帮助开发者更好地管理组件状态、副作用等,提升代码质量。@shined/react-use 就是这样一款优秀的 React Hooks 库。
本文旨在充分利用 @shined/react-use 提供的 Hooks ,呈现一份全面的基于 React Hooks 的代码质量提升指南,引导你走出 useEffect 和 useState 的舒适区,发掘 React Hooks 的真正潜力和灵活性,提升项目代码质量。
替换 useState
useState 是 React 中用来管理组件状态的 Hook ,基本上每个 React 开发者都会用到。
但是低版本 React 中的 useState 可能导致非预期的行为。比如在 React 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])
常见场景
防抖和节流
推荐使用 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)
处理事件
useEventListener 用于在组件挂载时添加事件监听器,组件卸载时自动移除事件监听器,适用于需要添加事件监听器的场景。任何实现了 EventTarget 接口的对象都可以作为第一个参数传入,例如 window、document、ref.current 等。
// 符合以下接口的对象都可以作为第一个参数传入,SSR 下支持 `() => window` 的写法
export interface InferEventTarget {
  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 })
复制到剪贴板
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 用于格式化时间,轻量、灵活、使用体验统一,适用于需要格式化时间的场景,支持自定义格式化字符串。
详情参考 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 })
浏览器 API
我们经常需要调用浏览器 API 来实现一些功能,包括但远不限于:
  • 使用 Fullscreen API 进行全屏操作
  • 使用 ResizeObserver API 监听元素尺寸变化
  • 使用 Network Information API 获取网络状态
  • 使用 EyeDropper API 获取屏幕颜色
  • 使用 Geolocation API 获取用户地理位置
  • 使用 Battery Status 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
    // 推荐
    const handleClick = useStableFn(() => {
      console.log('click')
    })
    return
    useCreation
    useCreation 用于初始化操作,适用于需要确保初始化操作只执行一次的场景,例如初始化复杂对象、耗时操作等,useCreation 除了性能优化外,还能确保结果在不同渲染周期间保持引用稳定。
    详情参考 useCreation。
    // 不推荐
    const heavyResult = useMemo(() => doHeavyWorkToInit(), [])
    const dynamicResult = useMemo(() => doHeavyWorkToCreate(), [dependency])
    // 推荐
    const heavyResult = useCreation(() => doHeavyWorkToInit())
    const dynamicResult = useCreation(() => doHeavyWorkToCreate(), [dependency])
    进阶指引
    如果你需要封装自定义 Hook ,或者需要更多高级功能,可以参考以下进阶指引。
    useStableFn
    参考上文。
    useLatest
    参考上文。
    useTargetElement
    useTargetElement 用于获取目标元素,适用于需要获取目标元素的场景,例如在自定义 Hook 中获取目标元素,确保使用体验的一致性。
    详情参考 useTargetElement 和 ElementTarget。
    const ref = useRef(null) //
    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 supported = useSupported('BatteryStatus')
    usePausable
    usePausable 用于创建一个 Pausable 实例,以赋予 Hooks 可暂停的能力,适用于需要暂停和恢复的场景。
    详情参考 usePausable。
    const pausable = usePausable(false, pauseCallback, resumeCallback)
    useGetterRef
    useGetterRef 暴露了一个函数以获取 ref.current 的最新值,适用于需要存储状态但不想触发重新渲染,同时需要获取最新值的场景。
    详情参考 useGetterRef。
    const [isActive, isActiveRef] = useGetterRef(false)
    写在最后
    useEffect 和 useState 确实是构建 React 应用的基础。然而,掌握更高级、安全、稳定的 Hooks 和相关技术,能让我们创建更为优雅和高效的应用、提高代码可读性、减少 bug 、提高开发幸福感。
    优秀的代码不仅仅是完成需求,更在于它的可读性、可维护性和扩展性。投资时间去学习新的工具和技术,不断重构和优化现有代码,是提升代码质量不可或缺的一环。鼓励将本指南作为起点,深入研究每一个概念,并在实践中不断尝试和反思。
    无论你是 React 新手还是有经验的开发者,希望通过本指南的学习和应用,你能够把 React 应用的质量提升到一个新的水平。记得,编程是一个不断学习和成长的过程,保持好奇心,不断挑战自己,你一定能够在这条道路上越走越远。
    祝 React 旅途愉快。
  • 您需要登录后才可以回帖 登录 | 立即注册

    返回顶部