Building a useWindowSize Hook from Scratch

building-a-usewindowsize-hook-from-scratch

Source code: reactuse/useWindowSize
Demo: https://www.reactuse.com/element/useWindowSize

In React development, we frequently need to adjust component behavior based on window size. Today, we’ll start with the simplest implementation and gradually optimize it to build a high-performance useWindowSize Hook.

Step 1: The Simplest Implementation

Let’s begin with the most basic version:

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
}

This version works, but has several issues:

  • Creates new objects on every window change, causing unnecessary re-renders
  • Doesn’t consider server-side rendering
  • Performance isn’t optimized

Step 2: Solving SSR Issues

During server-side rendering, there’s no window object, and we need to avoid hydration mismatch errors:

import { useState, useEffect } from 'react'

function useWindowSize() {
  // Key: Both server and client return the same initial value on first render
  const [windowSize, setWindowSize] = useState({
    width: 0,
    height: 0,
  })

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

    // Get real dimensions immediately on client
    updateSize()

    // Then listen for subsequent changes
    window.addEventListener('resize', updateSize)
    return () => window.removeEventListener('resize', updateSize)
  }, [])

  return windowSize
}

The key here is ensuring both server and client return the same value on first render to avoid hydration mismatch.

Step 3: Performance Optimization – Reducing Unnecessary Updates

Now let’s think about this question: If a component only uses width, should it re-render when height changes? The answer is no.

Let’s introduce the concept of dependency tracking:

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

      // Only check properties that the component actually uses
      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)
      }
    }

    // Get initial dimensions immediately
    handleResize()

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

  // Use getters to track dependencies
  return {
    get width() {
      stateDependencies.current.width = true
      return windowSize.width
    },
    get height() {
      stateDependencies.current.height = true
      return windowSize.height
    },
  }
}

The core idea here is: when a component accesses width or height, we record this dependency relationship, then only check the used properties when the window changes.

Step 4: Using useSyncExternalStore for Concurrent Safety

React 18 introduced useSyncExternalStore, specifically designed for synchronizing external state. Let’s refactor our code:

import { useRef } from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'

// Subscribe function
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 })

  // Comparison function: only compare used properties
  const isEqual = (prev: any, current: any) => {
    for (const key in stateDependencies) {
      if (current[key] !== prev[key]) {
        return false
      }
    }
    return true
  }

  const cached = useSyncExternalStore(
    subscribe, // Subscribe function
    () => {
      // Get current state
      const data = {
        width: window.innerWidth,
        height: window.innerHeight,
      }

      // If there's a change, update cache
      if (!isEqual(previous.current, data)) {
        previous.current = data
        return data
      }
      return previous.current
    },
    () => {
      // SSR fallback value - avoid hydration mismatch
      return { width: 0, height: 0 }
    },
  )

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

Step 5: Adding TypeScript Type Support

Finally, let’s add complete type definitions:

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-safe initial value
      return { width: 0, height: 0 }
    },
  )

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

Design Philosophy Summary

Throughout building this Hook, we followed these design principles:

  1. Start Simple: Implement basic functionality first, then optimize gradually
  2. Solve SSR Issues: Ensure server and client first render consistency to avoid hydration mismatch
  3. Performance Optimization: Use dependency tracking to reduce unnecessary re-renders
  4. Modern APIs: Leverage React 18 features for improved concurrent safety
  5. Type Safety: Add TypeScript support for better developer experience

Key Concepts Explained

Dependency Tracking System

The genius of this implementation lies in its dependency tracking system. By using getters, we can detect which properties a component actually uses and only trigger updates when those specific properties change.

SSR Compatibility

The key is ensuring both server-side rendering and client-side first render return the same initial values. The third parameter of useSyncExternalStore is specifically designed to provide SSR-safe initial values.

Smart Comparison Strategy

We maintain a cache and only update when necessary, significantly reducing memory allocations and render cycles.

Usage Examples

function MyComponent() {
  const { width, height } = useWindowSize()

  // Handle initial state (SSR or first load)
  if (width === 0 && height === 0) {
    return <div>Loading...div>
  }

  return (
    <div>
      <p>Width: {width}pxp>
      <p>Height: {height}pxp>
    div>
  )
}

// A component that only uses width won't re-render when height changes
function WidthOnlyComponent() {
  const { width } = useWindowSize()

  if (width === 0) {
    return <div>Loading...div>
  }

  return <div>Width: {width}pxdiv>
}

// Responsive layout
function ResponsiveLayout() {
  const { width } = useWindowSize()

  if (width === 0) {
    return <div>Loading...div>
  }

  return (
    <div>
      {width < 768 ? <MobileLayout /> : <DesktopLayout />}
    div>
  )
}

Performance Benefits

This implementation provides several performance advantages:

  1. Selective Updates: Only re-renders when accessed properties change
  2. Event Deduplication: Multiple components share the same event listener
  3. Memory Efficiency: Reuses objects when possible instead of creating new ones
  4. Concurrent Safety: Works correctly with React’s concurrent features

Real-World Applications

This Hook is perfect for:

  • Responsive component layouts
  • Conditional rendering based on screen size
  • Dynamic size calculations
  • Mobile adaptation logic
  • Performance monitoring dashboards

Through these steps, we started with the simplest implementation and gradually solved various problems, ultimately creating a high-performance, type-safe, SSR-compatible useWindowSize Hook that demonstrates modern React development best practices.

Source code: reactuse/useWindowSize
Demo: https://www.reactuse.com/element/useWindowSize

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
oop-is-the-middle-ages,-fp-is-the-enlightenment,-frp-is-impressionism,-sop-is-the-modern-minimalism

OOP is the middle ages, FP is the enlightenment, FRP is impressionism, SOP is the modern minimalism

Next Post
copilot-for.net:-ask-mode-vs-agent-mode-and-how-to-use-them

Copilot for .NET: Ask Mode vs Agent Mode and How to Use Them

Related Posts