从零开始编写 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...

相关推荐
张元清3 小时前
React useDropZone Hook:搭建一个文件拖放区(2026)
javascript·react.js
无人生还4 小时前
从 Vue3 到 React · 快速上手系列第 5 篇:事件处理与表单(受控组件 vs v-model)
前端·vue.js·react.js
光影少年5 小时前
RN Bridge 原理
前端·react native·react.js
moMo5 小时前
用 React 写一个 TodoList
react native·react.js
To_OC14 小时前
写了三遍 Todo List,我终于搞懂了 React 父子组件到底怎么通信
前端·javascript·react.js
kyriewen19 小时前
别再乱用useEffect了——你写的10个里有8个不该存在
前端·javascript·react.js
zandy101120 小时前
衡石 Agentic BI的ReAct 推理框架在 Agentic BI 中的工程化实践
前端·javascript·react.js
朝阳3921 小时前
react19【系列实用教程】JS 项目升级为 typescript
javascript·react.js·typescript
labixiong1 天前
React Compiler 用 Rust 重写了,编译提速 10 倍——手写 useMemo 的日子到头了
react.js·rust
张元清1 天前
React useDeepCompareEffect:修复 useEffect 的对象依赖问题(2026)
javascript·react.js