The Developer's Quarterly · React DebuggingVol. III · March 2026

Using window.addEventListener in React:The Main Usages and the Patterns That Matter

Window-level event listeners in React are best reserved for browser-wide concerns such as resize, scroll, shortcuts, visibility, navigation, and connectivity.

In React, most event handling belongs on elements, not on window. But there are a few cases where a global listener is the right tool: viewport resizing, scroll tracking, keyboard shortcuts, page visibility, navigation events, and browser online/offline state.

The common pattern is simple. Register the listener in an Effect, clean it up when the component unmounts, and keep the callback small enough to understand later.

The short version

Use window.addEventListener when the event belongs to the browser, not to a single component.

Typical examples include resize for viewport changes, scroll for page-level scroll state, keydown for global shortcuts, visibilitychange for tab focus and backgrounding, popstate and hashchange for navigation state, and online/offline for connection changes.

If the interaction is local to a button, input, or modal, keep the handler on the element or in React props. Global listeners are for global concerns.

The basic React pattern

The safest way to listen on window is inside useEffect.

Window width example
import { useEffect, useState } from 'react'; export function WindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { function handleResize() { setWidth(window.innerWidth); } window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, []); return <p>Window width: {width}px</p>; }

That pattern matters for two reasons. First, React can mount and unmount the component many times during development. Second, you do not want duplicate listeners hanging around after a route change or conditional render.

1. Window resize

The most common use of window.addEventListener in React is viewport measurement.

Viewport hook
import { useEffect, useState } from 'react'; export function useViewport() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() => { function handleResize() { setSize({ width: window.innerWidth, height: window.innerHeight, }); } window.addEventListener('resize', handleResize); handleResize(); return () => window.removeEventListener('resize', handleResize); }, []); return size; }

This is useful for responsive layouts, conditional mobile behavior, and small UI decisions that depend on the viewport rather than CSS alone. If CSS can solve the problem, prefer CSS. If the component needs to make logic decisions, resize is the right browser event.

2. Global scroll tracking

Scroll listeners are useful when you need to know whether the user has scrolled past a threshold, show a back-to-top button, or update a sticky header.

Scroll threshold hook
import { useEffect, useState } from 'react'; export function useScrolledPast(limit = 120) { const [pastLimit, setPastLimit] = useState(false); useEffect(() => { function handleScroll() { setPastLimit(window.scrollY > limit); } window.addEventListener('scroll', handleScroll, { passive: true }); handleScroll(); return () => window.removeEventListener('scroll', handleScroll); }, [limit]); return pastLimit; }

The important option here is passive: true. If you are only reading scroll position, tell the browser you will not call preventDefault(). That keeps scrolling smooth.

3. Keyboard shortcuts

Another common window-level usage is a global keyboard shortcut.

Global shortcut
import { useEffect } from 'react'; type ShortcutProps = { onCommandPalette: () => void; }; export function GlobalShortcuts({ onCommandPalette }: ShortcutProps) { useEffect(() => { function handleKeyDown(event: KeyboardEvent) { if ((event.metaKey || event.ctrlKey) && event.key === 'k') { event.preventDefault(); onCommandPalette(); } } window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onCommandPalette]); return null; }

This is the right choice for app-wide shortcuts, escape-to-close flows, and any interaction that should work regardless of which element is focused. If the shortcut should only work inside one component, keep it local instead.

4. Visibility and tab focus

visibilitychange, focus, and blur are useful when you need to know whether the user has left the tab or returned to it.

Tab visibility hook
import { useEffect, useState } from 'react'; export function useTabVisibility() { const [visible, setVisible] = useState(!document.hidden); useEffect(() => { function handleVisibilityChange() { setVisible(!document.hidden); } window.addEventListener('visibilitychange', handleVisibilityChange); return () => { window.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); return visible; }

This is useful for pausing timers, reducing polling, or refreshing stale state when the user comes back to the page. Many data libraries already do this internally, which is another sign that it is a browser-level concern.

5. Navigation events

popstate and hashchange are relevant when your app uses the browser history directly or needs to respond to URL fragment changes.

Hash listener
import { useEffect, useState } from 'react'; export function useCurrentHash() { const [hash, setHash] = useState(window.location.hash); useEffect(() => { function updateHash() { setHash(window.location.hash); } window.addEventListener('hashchange', updateHash); return () => window.removeEventListener('hashchange', updateHash); }, []); return hash; }

This is especially useful for in-page navigation, docs sites, and legacy routing patterns that still depend on the fragment identifier.

6. Online and offline state

Sometimes the most practical window listeners are the ones that reflect browser connectivity.

Connectivity hook
import { useEffect, useState } from 'react'; export function useOnlineStatus() { const [online, setOnline] = useState(navigator.onLine); useEffect(() => { function handleOnline() { setOnline(true); } function handleOffline() { setOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); return online; }

That is useful for chat apps, editors, dashboards, and any UI that needs to show a degraded state when the browser loses connectivity.

A practical comparison

Window event Main use in React What to watch for
resize Responsive logic and viewport measurements Prefer CSS if the logic is purely visual
scroll Back-to-top buttons, sticky state, scroll thresholds Use passive: true when only reading state
keydown Global shortcuts and escape handlers Clean up carefully so the shortcut does not leak across screens
visibilitychange Pause work when the tab is hidden Useful for timers and background refreshes
hashchange, popstate Routing and in-page navigation Usually unnecessary if your router already handles this
online, offline Connection status indicators Show useful UI, not just a banner

A reusable hook is usually better

Once you repeat the pattern more than once, extract it.

Reusable hook
import { useEffect } from 'react'; export function useWindowEvent<K extends keyof WindowEventMap>( type: K, handler: (event: WindowEventMap[K]) => void, options?: AddEventListenerOptions ) { useEffect(() => { window.addEventListener(type, handler as EventListener, options); return () => { window.removeEventListener(type, handler as EventListener, options); }; }, [type, handler, options]); }

That hook is not the only possible shape, but it shows the core idea: keep the add/remove logic together and give the component a clean API.

Why cleanup matters in React

React effects can run more than once in development, especially under Strict Mode. That is not a bug. It is a stress test. If your listener is not cleaned up properly, the extra setup cycle can reveal it.

This is also why anonymous functions are a bad fit for global listeners. If you cannot remove the listener with the same function reference, you will leak behavior across renders.

A few rules of thumb

Use window for browser state Resize, scroll, visibility, connectivity, and navigation belong to the browser environment.
Keep local interactions local Button clicks and input changes usually belong on the element itself.
Always clean up Remove the listener in the effect cleanup so it does not survive the component.
Prefer small handlers Do the minimum work in the event callback and move the logic somewhere testable.

Bottom line

The main uses of window.addEventListener in React are the ones that describe browser-wide state, not component-local interaction. Resize, scroll, keyboard shortcuts, visibility, routing, and network status are the common cases.

If you put the listener in an Effect, clean it up correctly, and keep the handler focused, window.addEventListener stays simple and predictable. That is the standard you want in React: use global listeners when the browser owns the event, and keep everything else inside the component tree.