📐

【React】ブラウザのウィンドウサイズを自動検知する

2024/02/17に公開

はじめに

以降にカスタムフックのコード例を記載しますが、全て以下のcreate-react-appで作成された初期のApp.tsxベースのコードで動きます。
どのカスタムフックのパターンでも呼び出し側のコードは同じで、同じ動作となります。

App.tsx
import "./App.css";
import { useWindowSize } from "./hooks/useWindowSize";

function App() {
  const { windowWidth, windowHeight } = useWindowSize();
  // const [windowWidth, windowHeight] = useWindowSize(); // 配列で返すパターンの場合


  return (
    <div className="App">
      <header className="App-header">
        <p>windowWidth: {windowWidth}</p>
        <p>windowHeight: {windowHeight}</p>
      </header>
    </div>
  );
}
export default App;


React18 以前

実装するにはuseStateと、useEffectuseLayoutEffect)を組み合わせて実装する方法がメジャーかなと思います。

以下にサンプルで 3 つのパターンを記載します。
やってることはほぼ一緒なので、好みやプロジェクトの方針に沿ったものを使用してやってください。

useWindowSize.ts(パターン1)
type WindowSize = {
  windowWidth: number;
  windowHeight: number;
};

export const useWindowSize = (): WindowSize => {
  const [windowSize, setWindowSize] = useState({
    windowWidth: window.innerWidth,
    windowHeight: window.innerHeight,
  });

  useEffect(() => {
    const resize = () => {
      setWindowSize({
        windowWidth: window.innerWidth,
        windowHeight: window.innerHeight,
      });
    };

    window.addEventListener("resize", resize);
    return () => window.removeEventListener("resize", resize);
  }, []);

  return windowSize;
};
useWindowSize.ts(パターン2)
export const useWindowSize = (): number[] => {
  const [windowSize, setWindowSize] = useState([0, 0]);

  useEffect(() => {
    const resize = (): void => {
      setWindowSize([window.innerWidth, window.innerHeight]);
    };

    window.addEventListener("resize", resize);
    return () => window.removeEventListener("resize", resize);
  }, []);

  return windowSize;
};
useWindowSize.ts(パターン3)
type WindowSize = {
  windowWidth: number;
  windowHeight: number;
};

export const useWindowSize = () => {
  const getWindowSize = () => {
    const { innerWidth: windowWidth, innerHeight: windowHeight } = window;

    return {
      windowWidth,
      windowHeight,
    };
  };

  const [windowSize, setWindowSize] = useState(getWindowSize());

  useEffect(() => {
    const resize = () => {
      setWindowSize(getWindowSize());
    };

    window.addEventListener("resize", resize);
    return () => window.removeEventListener("resize", resize);
  }, []);

  return windowSize;
};


useEffectuseLayoutEffect の違いの詳細はここでは割愛しますので、以下の記事などを参照ください。
(とりあえずuseEffectで実装しておいて、レンダリング時のちらつきやパフォーマンスに問題がある場合はuseLayoutEffectを検討するぐらいでいいのでは、と個人的には思ってます。)

https://zenn.dev/syu/articles/6b96e34535b33e

React18 以降

React18 から新たに追加されたuseSyncExternalStoreを使うことで、より簡単にウィンドウサイズを検知できるようになりました。
今まで useStateuseEffectなどを組みわせて実装していたものを、useSyncExternalStoreが内部的によしなにやってくれるようになります。

useSyncExternalStoreのことを簡単に説明すると、React の状態管理の管轄外(外部)にある状態を React の状態と同期させるためのフックです。
例えば、ウィンドウサイズ、ネットワーク状態、FireStore との同期、などなど。

詳細は公式ドキュメントを参照してください。

https://ja.react.dev/reference/react/useSyncExternalStore


useWindowSize.ts(実装例)
import { useSyncExternalStore } from "react";

type WindowSize = {
  windowWidth: number;
  windowHeight: number;
};

const getWindowWidth = () => {
  return window.innerWidth;
};
const getWindowHeight = () => {
  return window.innerHeight;
};

const subscribeWindowSizeChange = (callback: () => void) => {
  window.addEventListener("resize", callback);
  return () => window.removeEventListener("resize", callback);
};

export const useWindowSize = (): WindowSize => {
  const width = useSyncExternalStore(subscribeWindowSizeChange, getWindowWidth);
  const height = useSyncExternalStore(
    subscribeWindowSizeChange,
    getWindowHeight
  );
  return { windowWidth: width, windowHeight: height };
};


まとめ

  • React18 採用しているプロジェクトなら、useSyncExternalStoreを使うのが簡単で良さそう。
  • React18 未満のプロジェクトなら、useStateuseEffectの組み合わせて実装すれば対応可能。
    useEffectuseLayoutEffect かは、実装方針や方針に合わせて選択してください。
GitHubで編集を提案
NCDCエンジニアブログ

Discussion