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:
- Start Simple: Implement basic functionality first, then optimize gradually
- Solve SSR Issues: Ensure server and client first render consistency to avoid hydration mismatch
- Performance Optimization: Use dependency tracking to reduce unnecessary re-renders
- Modern APIs: Leverage React 18 features for improved concurrent safety
- 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:
- Selective Updates: Only re-renders when accessed properties change
- Event Deduplication: Multiple components share the same event listener
- Memory Efficiency: Reuses objects when possible instead of creating new ones
- 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