Chalarangelo/30-seconds-of-code

View on GitHub
content/snippets/react/s/window-global-environment-ssr-hooks.md

Summary

Maintainability
Test Coverage
---
title: React hooks to handle window and environment events
shortTitle: Window and environment hooks
type: story
language: react
tags: [hooks,state,effect,event,memo]
cover: jars-on-shelf-2
excerpt: Learn how to use React hooks to handle window events, media queries, server-side rendering and more.
listed: true
dateModified: 2024-06-29
---

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.

```jsx
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.

```jsx
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.

```jsx
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.

```jsx
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.

```jsx
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`.

```jsx
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.

```jsx
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()`.

```jsx
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 />
);
```