V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
VikiQAQ
V2EX  ›  前端开发

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

  •  
  •   VikiQAQ ·
    vikiboss · 126 天前 · 899 次点击
    这是一个创建于 126 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在 React 世界里,useEffectuseState 无疑是最常见的钩子( Hooks ),它们让函数组件拥有了类组件的力量。useState 赋予了组件维护状态的能力,而 useEffect 则让副作用管理变得简单。

    对许多开发者来说,上述两个 Hooks 是 React 钩子的入门级应用,但随着项目复杂度的提升,仅仅使用这两个钩子可能会让组件变得臃肿,逻辑复杂难以维护。此时,众多开源社区的 React Hooks 库应运而生,它们提供了许多高质量、语义化的 Hooks ,帮助开发者更好地管理组件状态、副作用等,提升代码质量。@shined/react-use 就是这样一款优秀的 React Hooks 库。

    本文旨在充分利用 @shined/react-use 提供的 Hooks ,呈现一份全面的基于 React Hooks 的代码质量提升指南,引导你走出 useEffectuseState 的舒适区,发掘 React Hooks 的真正潜力和灵活性,提升项目代码质量。

    替换 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 用于管理布尔值状态,提供了一系列语意化的操作函数,例如 togglesetTruesetFalse 等,底层使用 useSafeState 以确保状态安全。

    详情参考 useBoolean

    const [bool, actions] = useBoolean(false)
    
    actions.toggle() // true
    actions.setTrue() // true
    actions.setFalse() // false
    

    useCounter

    useCounter 用于管理 number 类型状态,提供了一系列语意化的操作函数,例如 incdecset 等,底层使用 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 用于在组件挂载时执行一次操作,在组件卸载时也执行一次操作,适用于只需要执行一次副作用的场景,本质上 useEffectOnceuseMountuseUnmount 的组合。

    详情参考 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])
    

    常见场景

    防抖和节流

    推荐使用 useDebouncedFnuseThrottledFn 两个 Hook 来处理常见的防抖和节流功能,当然也有 useDebouncedEffectuseDebouncedEffect 两个 Hook ,用于处理防抖和节流的副作用,但一般情况下,我们更推荐前者。

    详情参考 useDebouncedFnuseThrottledFn

    const handleSubmit = (value) => console.log(value)
    const debouncedHandleSubmit = useDebouncedFn(handleSubmit, 500)
    
    const handleScroll = (event) => console.log('scroll')
    const throttledHandleScroll = useThrottledFn(handleScroll, 500)
    

    处理事件

    useEventListener 用于在组件挂载时添加事件监听器,组件卸载时自动移除事件监听器,适用于需要添加事件监听器的场景。任何实现了 EventTarget 接口的对象都可以作为第一个参数传入,例如 windowdocumentref.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 })
    

    复制到剪贴板

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

    定时器

    日常开发中经常使用 setTimeoutsetInterval 来处理定时任务,直接使用相对繁琐,且要求开发者手动清理定时器,容易出现忘记清理、清理不及时等问题。

    // 不推荐
    useEffect(() => {
      const timer = setTimeout(() => {
        doSomething()
      }, 1000)
    
      return () => clearTimeout(timer)
    }, [])
    

    @shined/react-use 提供了 useTimeoutFnuseIntervalFn 两个 Hook 来处理定时任务,自动清理定时器,避免出现忘记清理、清理不及时等问题。

    详情参考 useTimeoutFnuseIntervalFn

    // 推荐
    useTimeoutFn(doSomething, 1000, { immediate: true })
    useIntervalFn(doSomething, 1000, { immediate: 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

    useCallbackuseMemo 是 React 中用于性能优化的 Hook ,常用来缓存函数和值,避免不必要的重复计算。

    但是在实际开发中,我们除了性能优化外,可能还需要确保引用稳定性,以避免不必要的副作用等问题。而根据官方文档,useCallbackuseMemo 仅供用于性能优化,并不保证引用稳定,因此在某些场景下可能会导致不稳定的行为。

    如果你需要优化性能的同时还想确保函数、结果的稳定,那么你可以尝试 useStableFnuseCreation

    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

    参考上文。

    useLatest

    参考上文。

    useTargetElement

    useTargetElement 用于获取目标元素,适用于需要获取目标元素的场景,例如在自定义 Hook 中获取目标元素,确保使用体验的一致性。

    详情参考 useTargetElementElementTarget

    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 supported = useSupported('BatteryStatus')
    

    usePausable

    usePausable 用于创建一个 Pausable 实例,以赋予 Hooks 可暂停的能力,适用于需要暂停和恢复的场景。

    详情参考 usePausable

    const pausable = usePausable(false, pauseCallback, resumeCallback)
    

    useGetterRef

    useGetterRef 暴露了一个函数以获取 ref.current 的最新值,适用于需要存储状态但不想触发重新渲染,同时需要获取最新值的场景。

    详情参考 useGetterRef

    const [isActive, isActiveRef] = useGetterRef(false)
    

    写在最后

    useEffectuseState 确实是构建 React 应用的基础。然而,掌握更高级、安全、稳定的 Hooks 和相关技术,能让我们创建更为优雅和高效的应用、提高代码可读性、减少 bug 、提高开发幸福感。

    优秀的代码不仅仅是完成需求,更在于它的可读性、可维护性和扩展性。投资时间去学习新的工具和技术,不断重构和优化现有代码,是提升代码质量不可或缺的一环。鼓励将本指南作为起点,深入研究每一个概念,并在实践中不断尝试和反思。

    无论你是 React 新手还是有经验的开发者,希望通过本指南的学习和应用,你能够把 React 应用的质量提升到一个新的水平。记得,编程是一个不断学习和成长的过程,保持好奇心,不断挑战自己,你一定能够在这条道路上越走越远。

    祝 React 旅途愉快。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   997 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 21:19 · PVG 05:19 · LAX 13:19 · JFK 16:19
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.