Reactで特定の要素の高さを取得するためのカスタムフックを作った : useContainerHeight

今回はreactで特定の要素の高さを取得したかったので、それをカスタムフック化してみました。

目次

CodeSandboxにサンプルを用意しました

サンプルでは今回のカスタムフックで取得した高さの表示と普通にoffsetHeightで取得した場合を表示しています。

サンプルでは下記の操作が行えます

  • 高さを再取得する
  • 要素を非表示にする
  • 要素の高さを変更する

useContainerHeight

特徴

このカスタムフックはoffsetHeightで普通に高さを取得する方法と比べて以下の違いがあります。

  • cssで非表示(display: none;)にされてる高さが取得できる
  • リサイズによって要素の高さが変化してもその都度高さを取得

コードの全体像

import { useState, useEffect, RefObject } from "react";

// 第2引数のオプションの型
type UseContainerHeightOptionProps = {
  // 任意の依存関係
  dependencies?: any[];
  // リサイズ時に高さの再計算を無効にする: trueで無効
  disableResizeCalculation?: boolean;
};

export const useContainerHeight = <T extends HTMLElement>(
  ref: RefObject<T>,
  { dependencies, disableResizeCalculation }: UseContainerHeightOptionProps = {}
): number => {
  // 要素の高さを保持するstate
  const [containerHeight, setContainerHeight] = useState(0);

  // 要素の高さを取得する
  const getHeight = () => {
    if (!ref.current) return;
    const targetContainer = ref.current;

    // 要素のコピーを作成し、親要素に追加する
    const ghostContainer = targetContainer.cloneNode(true) as T;
    targetContainer.parentNode?.appendChild(ghostContainer);

    // コピーのスタイルを設定して、高さを取得する
    ghostContainer.style.cssText =
      "display:block; height:auto; visibility:hidden;";
    const height = ghostContainer.offsetHeight;

    // コピーを削除する
    targetContainer.parentNode?.removeChild(ghostContainer);

    // 要素の高さをstateに設定する
    setContainerHeight(height);
  };

  // コンポーネントのマウント時とdisableResizeCalculation変更時にイベントリスナーを登録&削除する
  useEffect(() => {
    if (disableResizeCalculation) return;
    // リサイズ時に要素の高さを再計算する
    window.addEventListener("resize", getHeight);
    return () => {
      window.removeEventListener("resize", getHeight);
    };
  }, [ref, disableResizeCalculation]);

  // コンポーネントのマウント時と任意の依存関係が変化したときにも要素の高さを計算する
  useEffect(
    () => {
      getHeight();
    },
    dependencies ? dependencies : []
  );

  return containerHeight;
};

使い方

まずはサンプルコードの全体

以下のコードが基本的な使い方になります

import { useRef } from "react";
import { useContainerHeight } from "path/to/useContainerHeight";

const Home = () => {
  // 高さを取得したい要素
  const ref = useRef<HTMLDivElement>(null);

  // カスタムフックで高さを取得
  const containerHeight = useContainerHeight(ref);

  return (
    <div>
      <div ref={ref}>
        高さを取得したい要素
      </div>
      <p>高さ:{containerHeight}</p>
    </div>
  );
};

export default Home;

大したコード量ではないですが、次でこのコードを分解して見てみます!

インポート

import { useRef } from "react";
import { useContainerHeight } from "path/to/useContainerHeight";

先程のuseContainerHeightをインポート
そしてrefを扱うのでuseRefもインポートしておきます。

高さを取得したい要素のrefを取得

useRefを使って高さを取得したい要素のrefを取得します

const Home = () => {

  // 高さを取得したい要素
  const ref = useRef<HTMLDivElement>(null);

  return (
    <div>
      <div ref={ref}>
        高さを取得したい要素
      </div>
    </div>
  );
};

export default Home;

先程のrefをuseContainerHeightで使い高さを取得

refをuseContainerHeightの第1引数にセットするだけ

その戻り値に高さが設定されているので、以下はcontainerHeightに高さが入ります。

const containerHeight = useContainerHeight(ref);

以上で使い方の説明は終わりです。

本当にただuseContainerHeightの第1引数にrefをセットするだけなので簡単です!

オプション

このカスタムフックの第2引数はちょっとしたオプションを用意しています。

プロパティ詳細デフォルト
dependencies任意の依存関係を設定できる。その設定した値が更新されたときに高さを再取得する。useEffectの第2引数と同じ[]any[]
disableResizeCalculationリサイズによる再計算を止めるfalseboolean

書き方

以下のように第2引数にオブジェクトで使いたいプロパティを設定します。

const containerHeight = useContainerHeight<HTMLAnchorElement>(ref, {
  dependencies: [hoge, fuga], //任意の依存関係
  disableResizeCalculation: true //リサイズに再計算しない
});

終わりに

今回は高さを取得するためのカスタムフックuseContainerHeightを紹介してみました。

紹介したはいいですが、実際にどういうときに使うかって話ですが、僕はアコーディオンを実装するときに使っています。

アコーディオンは開閉アニメーションを付ける場合に高さがわからないとcssのtransitionが効かないからです。

そのときに重要なのが非表示の要素の高さを取得することです。アコーディオンは開く前に開いたときの高さを取得する必要があります。
そこで今回のuseContainerHeightを使うことで閉じている状態でも高さを取得することができるためcssのtransitionでアニメーションを実装することができます。

という経緯で出来上がったのが今回カスタムフックになります。以上です!