かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React window リサイズ時にリスト要素の高さを揃えたい

カルーセルとか横並びの要素の高さをReactで揃えたいような場合 (display: flex が使えないようなケース)
要素のそれぞれのDOMにアクセスして高さを取得して最も高いものを取得しなければならないので、リストのような Ref が必要になります。
また、ウィンドウサイズが変わった時に再計算する必要があるので、 resize イベントで最大値の再計算をする必要もあります。

useRef で複数の要素に ref を渡す

DOMへのアクセスは RefObject を通じてアクセスできますが、リスト要素それぞれのDOMにアクセスしたいとなると、それぞれに ref を渡す必要があります。

import React, { useState, useRef, createRef } from 'react';

function Carousel({ initItems }) {
  const [items, setItems] = useState(initItems);
  // current に refObject の入った配列を作成する
  const itemsRef = useRef(
    [...Array(items.length)].map(() => createRef)
  );

  const carouselItems = items.map(({id, ...props}, i) => {
    return (
      <CarouselItem
        key={id}
        ref={itemsRef.current[i]}
        {...props}
      />
    );
  });
  
  return (
    <div className="carousel">
      {carouselItems}
    </div>
  );
}

ref で DOMにアクセスして最大値の高さを設定する

itemsRef.current を reduce で回して一番大きな値を取得してそれぞれのDOMにそれを設定すればOK

import React, { useState, useEffect, useCallback, useRef, createRef } from 'react';

function Carousel({ initItems }) {
  const [items, setItems] = useState(initItems);
  const itemsRef = useRef(
    [...Array(items.length)].map(() => createRef)
  );

  const getMaxHeight = useCallback((refList) => {
    return refList.reduce((maxHeight, ref) => {
      if ( !ref.current ) { return maxHeight; }
      const itemHeight = ref.current.offsetHeight;
      return Math.max(itemHeight, maxHeight);
    }, 0);
  }, []);
  
  const setMaxHeight = useCallback(() => {
    itemRefs.current.forEach((ref) => {
      if (ref.current) {
        ref.current.removeAttribute('style');
      }
    });
    
    const max = getMaxHeight(itemRefs.current);
    
    if ( max ) {
      itemRefs.current.forEach((ref) => {
        ref.current.style.height = `${max}px`;
      });
    }
  }, [itemRefs]);
  
  // 初回レンダー時に高さを揃える処理を実行する
  useEffect(() => {
    setMaxHeight();
  }, []);

  //...
  return (...);
}

window resize 時にも高さを揃える処理を実行する

resize イベントが起こる度に処理をしていると処理が重くなるので debounce で実行する
cf.

今回は関数を実行するだけで、値を返す必要がないので、useDebounceFn という custom hook を作成しました

// useDebounceFn.js
import { useRef, useCallback } from 'react';

const useDebounceFn = (fn, delay = 100) => {
  const timer = useRef(null);
  
  const dispatch = useCallback((_val) => {
    timer.current && clearTimeout(timer.current);
    timer.current = setTimeout(() => {
      fn(_val);
    }, delay);
  }, [fn, delay, timer]);
  
  return [dispatch];
};

export useDebounceFn;

window resize イベントは useEffect 内で設定して、コンポーネントが unmount された時にイベントが外れるように clean up の処理を return するようにします。

import React, { useState, useEffect, useCallback, useRef, createRef } from 'react';
import { useDebounceFn } from './useDebounceFn';

function Carousel({ initItems }) {
  const [items, setItems] = useState(initItems);
  const itemsRef = useRef(
    [...Array(items.length)].map(() => createRef)
  );

  const getMaxHeight = useCallback((refList) => {
    return refList.reduce((maxHeight, ref) => {
      if ( !ref.current ) { return maxHeight; }
      const itemHeight = ref.current.offsetHeight;
      return Math.max(itemHeight, maxHeight);
    }, 0);
  }, []);
  
  const setMaxHeight = useCallback(() => {
    itemRefs.current.forEach((ref) => {
      if (ref.current) {
        ref.current.removeAttribute('style');
      }
    });
    
    const max = getMaxHeight(itemRefs.current);
    
    if ( max ) {
      itemRefs.current.forEach((ref) => {
        ref.current.style.height = `${max}px`;
      });
    }
  }, [itemRefs]);

  // resize 時に debounce で実行するイベント
  const [onResizeHandler] = useDebounceFn(setMaxHeight);
  
  useEffect(() => {
    window.addEventListener('resize', onResizeHandler);
    setMaxHeight();
    // unmount 時に実行する処理
    return () => window.removeEventListener('resize', onResizeHandler);
  }, []);

  //...
  return (...);
}

sample

👇 window.resize がいるので別タブとかで開いてみてください

https://codepen.io/kikiki_kiki/pen/povxxeN

 
useRef は色んなものを入れれる箱だと認識してたけど、その中に配列で ref を入れればループで出力するようなコンポーネントにも ref を渡せるってのに気づくのが難しかった


[参考]

アイカツ!シリーズ 5thフェスティバル!! Day1 Blu-ray

アイカツ!シリーズ 5thフェスティバル!! Day1 Blu-ray

START DASH SENSATION マジ神曲でモチベーション上がる曲。聴いて!そしてライブも観て!!