React hooks to handle window and environment events
Handling window and environment events and changes in React components is definitely not a one-size-fits-all solution. However, React hooks can make it easier to manage these events and changes in a more organized way.
useUnload
hook
The useUnload
hook is used to handle the beforeunload
window event. This event is triggered when the user is about to close the window.
In order to implement this hook, you'll need to use the useRef()
and useEffect()
hooks. The useRef()
hook is used to create a ref for the callback function, fn
. The useEffect()
hook is used to handle the 'beforeunload'
event by using EventTarget.addEventListener()
. Finally, EventTarget.removeEventListener()
is used to perform cleanup after the component is unmounted.
const useUnload = fn => { const cb = React.useRef(fn); React.useEffect(() => { const onUnload = cb.current; window.addEventListener('beforeunload', onUnload); return () => { window.removeEventListener('beforeunload', onUnload); }; }, [cb]); }; const App = () => { useUnload(e => { e.preventDefault(); const exit = confirm('Are you sure you want to leave?'); if (exit) window.close(); }); return <div>Try closing the window.</div>; }; ReactDOM.createRoot(document.getElementById('root')).render( <App /> );
useOnline
hook
To check if a client is online or offline, you can use the useOnline
hook. This hook uses the Navigator.onLine
web API to get the online status of the client.
Implementation-wise, you'll need to create a function, getOnLineStatus
, that uses the Navigator.onLine
web API to get the online status of the client. You'll also need to use the useState()
hook to create an appropriate state variable, status
, and setter.
The useEffect()
hook is used to add listeners for appropriate events, updating state, and cleanup those listeners when unmounting. Finally, the status
state variable is returned.
const getOnLineStatus = () => typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean' ? navigator.onLine : true; const useOnline = () => { const [status, setStatus] = React.useState(getOnLineStatus()); const setOnline = () => setStatus(true); const setOffline = () => setStatus(false); React.useEffect(() => { window.addEventListener('online', setOnline); window.addEventListener('offline', setOffline); return () => { window.removeEventListener('online', setOnline); window.removeEventListener('offline', setOffline); }; }, []); return status; }; const StatusIndicator = () => { const isOnline = useOnline(); return <span>You are {isOnline ? 'online' : 'offline'}.</span>; }; ReactDOM.createRoot(document.getElementById('root')).render( <StatusIndicator /> );
useWindowSize
hook
Tracking the browser window's dimensions can be useful for responsive design. The useWindowSize
hook can be used to track the dimensions of the browser window.
Using the useState()
hook, you can initialize a state variable that will hold the window's dimensions. Initialize with both values set to undefined
to avoid mismatch between server and client renders.
Then, create a function that uses Window.innerWidth
and Window.innerHeight
to update the state variable. Finally, use the useEffect()
hook to set an appropriate listener for the 'resize'
event on mount and clean it up when unmounting.
const useWindowSize = () => { const [windowSize, setWindowSize] = React.useState({ width: undefined, height: undefined, }); React.useEffect(() => { const handleResize = () => setWindowSize({ width: window.innerWidth, height: window.innerHeight }); window.addEventListener('resize', handleResize); handleResize(); return () => { window.removeEventListener('resize', handleResize); }; }, []); return windowSize; }; const MyApp = () => { const { width, height } = useWindowSize(); return ( <p> Window size: ({width} x {height}) </p> ); }; ReactDOM.createRoot(document.getElementById('root')).render( <MyApp /> );
useOnWindowResize
All's good and well, but what about when you want to execute a callback whenever the window is resized? The useOnWindowResize
hook can help you with that.
In order to implement it, you'll need to listen for the 'resize'
event on the Window
global object. The useRef()
hook is used to create a variable, listener
, which will hold the listener reference.
The useEffect()
hook is used to listen to the 'resize'
event of the Window
global object. Finally, EventTarget.removeEventListener()
is used to remove any existing listeners and clean up when the component unmounts.
const useOnWindowResize = callback => { const listener = React.useRef(null); React.useEffect(() => { if (listener.current) window.removeEventListener('resize', listener.current); listener.current = window.addEventListener('resize', callback); return () => { window.removeEventListener('resize', listener.current); }; }, [callback]); }; const App = () => { useOnWindowResize(() => console.log(`window size: (${window.innerWidth}, ${window.innerHeight})`) ); return <p>Resize the window and check the console</p>; }; ReactDOM.createRoot(document.getElementById('root')).render( <App /> );
useOnWindowScroll
hook
Similar to the useOnWindowResize
hook, the useOnWindowScroll
hook executes a callback whenever the window is scrolled.
The implementation is virtually identical to the previous hook, except that it listens for the 'scroll'
event instead of the 'resize'
event.
const useOnWindowScroll = callback => { const listener = React.useRef(null); React.useEffect(() => { if (listener.current) window.removeEventListener('scroll', listener.current); listener.current = window.addEventListener('scroll', callback); return () => { window.removeEventListener('scroll', listener.current); }; }, [callback]); }; const App = () => { useOnWindowScroll(() => console.log(`scroll Y: ${window.pageYOffset}`)); return <p style={{ height: '300vh' }}>Scroll and check the console</p>; }; ReactDOM.createRoot(document.getElementById('root')).render( <App /> );
useMediaQuery
hook
Speaking of responsive design, the useMediaQuery
hook can be used to check if the current environment matches a given media query and return the appropriate value.
In order to implement this hook, you'll need to check if Window
and Window.matchMedia()
exist. If not, return whenFalse
(e.g., in an SSR environment or unsupported browser).
Use Window.matchMedia()
to match the given query
. Cast its matches
property to a boolean and store it in a state variable, match
, using the useState()
hook.
Then, use the useEffect()
hook to add a listener for changes and clean up the listeners after the hook is destroyed. Finally, return either whenTrue
or whenFalse
based on the value of match
.
const useMediaQuery = (query, whenTrue, whenFalse) => { if (typeof window === 'undefined' || typeof window.matchMedia === 'undefined') return whenFalse; const mediaQuery = window.matchMedia(query); const [match, setMatch] = React.useState(!!mediaQuery.matches); React.useEffect(() => { const handler = () => setMatch(!!mediaQuery.matches); mediaQuery.addListener(handler); return () => mediaQuery.removeListener(handler); }, []); return match ? whenTrue : whenFalse; }; const ResponsiveText = () => { const text = useMediaQuery( '(max-width: 400px)', 'Less than 400px wide', 'More than 400px wide' ); return <span>{text}</span>; }; ReactDOM.createRoot(document.getElementById('root')).render( <ResponsiveText /> );
useSSR
hook
As we've already touched upon the subject of server-side rendering (SSR), it's worth mentioning the useSSR
hook. This hook can be used to check if the code is running on the browser or the server.
To implement this hook, you'll need to check if Window
, Window.document
, and Document.createElement()
exist. Use the useState()
hook to define the inBrowser
state variable and store the result of the check in it.
Then, use the useEffect()
hook to update the inBrowser
state variable and clean up at the end. Finally, use the useMemo()
hook to memoize the return values of the custom hook.
const isDOMavailable = !!( typeof window !== 'undefined' && window.document && window.document.createElement ); const useSSR = () => { const [inBrowser, setInBrowser] = React.useState(isDOMavailable); React.useEffect(() => { setInBrowser(isDOMavailable); return () => { setInBrowser(false); }; }, []); const useSSRObject = React.useMemo( () => ({ isBrowser: inBrowser, isServer: !inBrowser, canUseWorkers: typeof Worker !== 'undefined', canUseEventListeners: inBrowser && !!window.addEventListener, canUseViewport: inBrowser && !!window.screen }), [inBrowser] ); return React.useMemo( () => Object.assign(Object.values(useSSRObject), useSSRObject), [inBrowser] ); }; const SSRChecker = props => { let { isBrowser, isServer } = useSSR(); return <p>{isBrowser ? 'Running on browser' : 'Running on server'}</p>; }; ReactDOM.createRoot(document.getElementById('root')).render( <SSRChecker /> );
useIsomorphicEffect
hook
A further use-case for handling window and environment events is the useIsomorphicEffect
hook. This hook resolves to useEffect()
on the server and useLayoutEffect()
on the client.
To implement this hook, use typeof
to check if the Window
object is defined. If it is, return the useLayoutEffect()
. Otherwise, return useEffect()
.
const useIsomorphicEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; const MyApp = () => { useIsomorphicEffect(() => { window.console.log('Hello'); }, []); return null; }; ReactDOM.createRoot(document.getElementById('root')).render( <MyApp /> );