从零开始编写 useWindowSize Hook

代码来源:reactuse/useWindowSize

Demo地址:www.reactuse.com/element/use...

在 React 开发中,我们经常需要根据窗口大小来调整组件的行为。今天我们将从最简单的实现开始,逐步优化,最终构建出一个高性能的 useWindowSize Hook。

第一步:最简单的实现

让我们从最基础的版本开始:

javascript 复制代码
import { useState, useEffect } from 'react'

function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  })

  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      })
    }

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return windowSize
}

这个版本能工作,但存在几个问题:

  • 每次窗口变化都会创建新对象,导致不必要的重新渲染
  • 没有考虑服务端渲染
  • 性能不够优化

第二步:解决 SSR 问题

服务端渲染时没有 window 对象,而且需要避免 hydration mismatch 错误:

javascript 复制代码
import { useState, useEffect } from 'react'

function useWindowSize() {
  // 关键:服务端和客户端首次渲染都返回相同的初始值
  const [windowSize, setWindowSize] = useState({
    width: 0,
    height: 0,
  })

  useEffect(() => {
    function updateSize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      })
    }

    // 客户端首次执行时立即获取真实尺寸
    updateSize()

    // 然后监听后续变化
    window.addEventListener('resize', updateSize)
    return () => window.removeEventListener('resize', updateSize)
  }, [])

  return windowSize
}

这里的关键是确保服务端和客户端首次渲染时返回相同的值,避免 hydration mismatch。

第三步:性能优化 - 减少不必要的更新

现在我们思考一个问题:如果组件只使用了 width,那么 height 变化时是否需要重新渲染?答案是不需要。

让我们引入依赖追踪的概念:

javascript 复制代码
import { useRef, useState, useEffect } from 'react'

function useWindowSize() {
  const stateDependencies = useRef<{ width?: boolean; height?: boolean }>({})
  
  const [windowSize, setWindowSize] = useState({
    width: 0,
    height: 0,
  })

  const previousSize = useRef(windowSize)

  useEffect(() => {
    function handleResize() {
      const newSize = {
        width: window.innerWidth,
        height: window.innerHeight,
      }

      // 只检查组件实际使用的属性
      let shouldUpdate = false
      for (const key in stateDependencies.current) {
        if (newSize[key as keyof typeof newSize] !== previousSize.current[key as keyof typeof newSize]) {
          shouldUpdate = true
          break
        }
      }

      if (shouldUpdate) {
        previousSize.current = newSize
        setWindowSize(newSize)
      }
    }

    // 立即获取初始尺寸
    handleResize()

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  // 使用 getter 来追踪依赖
  return {
    get width() {
      stateDependencies.current.width = true
      return windowSize.width
    },
    get height() {
      stateDependencies.current.height = true
      return windowSize.height
    },
  }
}

这里的核心思路是:当组件访问 widthheight 时,我们记录下这个依赖关系,然后在窗口变化时只检查被使用的属性。

第四步:使用 useSyncExternalStore 提升并发安全性

React 18 引入了 useSyncExternalStore,专门用于同步外部状态,让我们重构代码:

typescript 复制代码
import { useRef } from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'

// 订阅函数
function subscribe(callback: () => void) {
  window.addEventListener('resize', callback)
  return () => {
    window.removeEventListener('resize', callback)
  }
}

function useWindowSize() {
  const stateDependencies = useRef<{ width?: boolean; height?: boolean }>({}).current
  const previous = useRef({ width: 0, height: 0 })

  // 比较函数:只比较被使用的属性
  const isEqual = (prev: any, current: any) => {
    for (const key in stateDependencies) {
      if (current[key] !== prev[key]) {
        return false
      }
    }
    return true
  }

  const cached = useSyncExternalStore(
    subscribe, // 订阅函数
    () => {
      // 获取当前状态
      const data = {
        width: window.innerWidth,
        height: window.innerHeight,
      }
      
      // 如果有变化,更新缓存
      if (!isEqual(previous.current, data)) {
        previous.current = data
        return data
      }
      return previous.current
    },
    () => {
      // SSR 回退值 - 避免 hydration mismatch
      return { width: 0, height: 0 }
    },
  )

  return {
    get width() {
      stateDependencies.width = true
      return cached.width
    },
    get height() {
      stateDependencies.height = true
      return cached.height
    },
  }
}

第五步:添加 TypeScript 类型支持

最后,让我们添加完整的类型定义:

typescript 复制代码
import { useRef } from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'

interface WindowSize {
  width: number
  height: number
}

interface StateDependencies {
  width?: boolean
  height?: boolean
}

interface UseWindowSize {
  (): {
    readonly width: number
    readonly height: number
  }
}

function subscribe(callback: () => void) {
  window.addEventListener('resize', callback)
  return () => {
    window.removeEventListener('resize', callback)
  }
}

export const useWindowSize: UseWindowSize = () => {
  const stateDependencies = useRef<StateDependencies>({}).current
  const previous = useRef<WindowSize>({
    width: 0,
    height: 0,
  })

  const isEqual = (prev: WindowSize, current: WindowSize) => {
    for (const _ in stateDependencies) {
      const t = _ as keyof StateDependencies
      if (current[t] !== prev[t]) {
        return false
      }
    }
    return true
  }

  const cached = useSyncExternalStore(
    subscribe,
    () => {
      const data = {
        width: window.innerWidth,
        height: window.innerHeight,
      }
      if (!isEqual(previous.current, data)) {
        previous.current = data
        return data
      }
      return previous.current
    },
    () => {
      // SSR 安全的初始值
      return { width: 0, height: 0 }
    },
  )

  return {
    get width() {
      stateDependencies.width = true
      return cached.width
    },
    get height() {
      stateDependencies.height = true
      return cached.height
    },
  }
}

设计思路总结

在构建这个 Hook 的过程中,我们遵循了以下设计思路:

  1. 从简单开始:先实现基本功能,再逐步优化
  2. 解决 SSR 问题:确保服务端和客户端首次渲染一致,避免 hydration mismatch
  3. 性能优化:通过依赖追踪减少不必要的重新渲染
  4. 现代化 API:使用 React 18 的新特性提升并发安全性
  5. 类型安全:添加 TypeScript 支持提供更好的开发体验

关键概念解释

依赖追踪系统

这个实现的精髓在于依赖追踪系统。通过使用 getter 函数,我们可以检测组件实际使用了哪些属性,并且只在这些特定属性发生变化时才触发更新。

SSR 兼容性

关键是确保服务端渲染和客户端首次渲染返回相同的初始值。useSyncExternalStore 的第三个参数专门用于提供 SSR 安全的初始值。

智能比较策略

我们维护一个缓存,只在必要时更新,显著减少了内存分配和渲染周期。

使用示例

javascript 复制代码
function MyComponent() {
  const { width, height } = useWindowSize()
  
  // 处理初始状态(SSR 或首次加载)
  if (width === 0 && height === 0) {
    return <div>加载中...</div>
  }
  
  return (
    <div>
      <p>宽度: {width}px</p>
      <p>高度: {height}px</p>
    </div>
  )
}

// 只使用宽度的组件不会因为高度变化而重新渲染
function WidthOnlyComponent() {
  const { width } = useWindowSize()
  
  if (width === 0) {
    return <div>加载中...</div>
  }
  
  return <div>宽度: {width}px</div>
}

// 响应式布局
function ResponsiveLayout() {
  const { width } = useWindowSize()
  
  if (width === 0) {
    return <div>加载中...</div>
  }
  
  return (
    <div>
      {width < 768 ? <MobileLayout /> : <DesktopLayout />}
    </div>
  )
}

性能优势

这个实现提供了几个性能优势:

  1. 选择性更新:只有访问的属性变化时才重新渲染
  2. 事件去重:多个组件共享同一个事件监听器
  3. 内存效率:尽可能重用对象而不是创建新对象
  4. 并发安全:与 React 的并发特性完美配合

通过这样的步骤,我们从最简单的实现开始,逐步解决了各种问题,最终得到了一个高性能、类型安全、SSR 兼容的 useWindowSize Hook。


代码来源:reactuse/useWindowSize

Demo地址:www.reactuse.com/element/use...

相关推荐
福娃B16 分钟前
【React】React 状态管理与组件通信:Zustand vs Redux📦
前端·react.js·前端框架
挽淚22 分钟前
基于React框架的移动端UI组件库:React-Vant
react.js
PineappleCoder7 小时前
性能优化与状态管理:React的“加速器”与“指挥家”
前端·react.js
讨厌吃蛋黄酥7 小时前
深度解析:useContext + useReducer — React官方状态管理的终极之道
javascript·react.js·前端框架
bug_kada7 小时前
全家桶开发之Zustand:轻量级状态管理
前端·react.js
伍哥的传说10 小时前
React性能优化终极指南:memo、useCallback、useMemo全解析
前端·react.js·性能优化·usecallback·usememo·react.memo·react devtools
土豆125019 小时前
React Router 相对路径避坑指南:v5 到 v6 的颠覆性变革!
react.js
三月的一天20 小时前
React+threejs两种3D多场景渲染方案
前端·react.js·前端框架
十盒半价1 天前
React 项目实战:从 0 到 1 构建高效 GitHub 仓库管理应用 —— 基于 React 全家桶的全栈开发指南
前端·react.js·trae
讨厌吃蛋黄酥1 天前
React高手都在用的秘密武器:Fragment的5大逆天功能,让你的性能飙升200%!
前端·javascript·react.js