🐈

アコーディオンコンポーネントをReactで自作する

2023/04/07に公開

こんにちは!CastingONE の岡本です。

はじめに

弊社のフロントエンド開発は Vue.js を使用していますが、React に移行しようとしています。わたしは React 未経験なので日々勉強しています。React 力を高めるためにコンポーネントを自作することを考えましたが、丁度Vue.js で自作アコーディオンを作成する記事がありましたので、それの React 版を作ってみました。

今回学んだ機能

このコンポーネント作成を通じて学んだ機能を紹介します。

useRef

Vue.js だとテンプレート内の要素を DOM として操作したい場合にはref属性を指定することで、script 内で$refsの中で DOM を呼び出すことができました。

React だとuseRefというフックを使って、currentキーを指定することでで DOM を参照することができます。

useRef
export const UseRef = () => {
  // useRefフックを使用
  const sampleRef = useRef<HTMLDivElement | null>(null)
  // DOMの参照はcurrentキーを指定する
  console.log(sampleRef.current)
  // ↓↓↓
  // <div>サンプル</div>

  return (
    <div>
      // 参照したいDOM要素のref属性にsampleRefを当てる
      <div ref={sampleRef}>サンプル</div>
    </div>
  )
}

onTransitionEnd イベント

onTransitionEnd イベントは CSS アニメーションにおいて CSS Transition プロパティの実行が完了したら発生するイベントです。使用方法は以下の通りです。

onTransitionEnd
return (
  <div
    style={{
      transition: 'all 2s',
      opacity: '1'
    }}
    // CSS Transitionが終わったら発火する
    onTransitionEnd={() => {
      // CSS Transitionが終わったら行う処理
    }}
  >
    OnTransitionEnd
  </div>
)

実装方法

コンポーネントの DOM 構成はまず、アコーディオンの中身をラップするelContentと、それをアコーディオンするelAccordionを使用する構成にします。
また、アコーディオンの開閉フラグとアコーディオンの中身を props で受け取るようにします。

コンポーネントの全体設計
type Props = {
  /** アコーディオン開閉フラグ */
  isOpen: boolean
  /** アコーディオンの中身 */
  children: ReactNode
}
export const Accordion: FC<Props> = (props) => {
  return (
    <div>  {/* ← elAccordion */}
      <div>  {/* ← elContent */}
        {props.children}
      </div>
    </div>
  )
}

開く処理

初期状態はアコーディオンを閉じている状態なので elAccordion の高さを0pxにし、overflow: hiddenをスタイルに当てて中身が見えないようにします。

Accordion.tsx
// 初期状態(閉じている状態)は0pxを設定する
const [heightStyle, setHeightStyle] = useState<string>(
  props.isOpen ? "" : "0px"
);
// 初期状態はoverflow: hiddenを設定する
const [isOverflowHidden, setIsOverflowHidden] = useState<boolean>(!props.isOpen)

この状態で以下の手順を踏んで開くアニメーションをします。

  1. useRef を使って elContent の高さを取得して、その値をセットする
Accordion.tsx
const [heightStyle, setHeightStyle] = useState<string>(
  props.isOpen ? "" : "0px"
);
const elContentRef = useRef<HTMLDivElement | null>(null);

// clientRefを使って要素の高さをセット
setHeightStyle(`${elContentRef.current.clientHeight}px`;);
  1. CSS Transition で目標の高さの値までアニメーションする
Accordion.tsx
return (
  <div
    ref={elAccordionRef}
    style={{
      height: heightStyle,
      overflow: isOverflowHidden ? 'hidden' : ''
      // CSS Transitionを設定
      transition: 'height 0.5s'
    }}
  >
    <div ref={elContentRef}>{props.children}</div>
  </div>
)
  1. onTransitionEnd を使ってアニメーション終了後スタイルをリセットする

スタイルをリセットしないと開いた後にアコーディオンの中身のサイズが変わったとしても、調整されなくなってしまうので、onTransitionEndイベントを使って以下の処理を行います。

Accordion.tsx
// 開いた時は高さとoverflowの設定を解除
const handleTransitionEnd = () => {
  if (props.isOpen) {
    setHeightStyle('');
    setIsOverflowHidden(false);
  }
};

return {
  <div
    ref={elAccordionRef}
    // ...
    // onTransitionEndイベントを使う
    onTransitionEnd={handleTransitionEnd}
  >
    {/* .... */}
  </div>
}

閉じる処理

閉じる場合は、以下の手順を踏みます。

  1. アコーディオン全体の今の高さを取得し、その値をセットする。その後ワンテンポ置いてから 0px をセットする。

useEffectを使用して、アコーディオン開閉フラグを監視し、フラグが falseの時に0pxをセットします。
ワンテンポ置く理由は、0px をセットしないと、いきなり 0px がセットされる挙動になり、アニメーションが動作しないためです。

Accordion.tsx
useEffect(() => {
  // アコーディオンの高さを取得して、セットする
  setHeightStyle(`${elAccordionRef.current.clientHeight}px`)

  // setTimeoutを使ってワンテンポ置いて0pxをセットする
  setTimeout(() => {
    setHeightStyle(() => props.isOpen ? `${elContentRef.current.clientHeight}px` : "0px")
  }, 100)
}, [props.isOpen])
  1. CSS Transition で 0px に向かってアニメーションされる
  2. 閉じた場合はスタイルのリセットは行わない(リセットすると中身が見えてしまうため)

まとめ

開く処理、閉じる処理をまとめるとAccordion.tsxは以下のようなコードになります。

Accordion.tsx
import { FC, ReactNode, useEffect, useRef, useState } from "react";

type Props = {
  /** アコーディオン開閉フラグ */
  isOpen: boolean
  /** アコーディオンの中身 */
  children: ReactNode
};

export const Accordion: FC<Props> = (props) => {
  const [heightStyle, setHeightStyle] = useState<string>(
    props.isOpen ? "" : "0px"
  );
  const [isOverflowHidden, setIsOverflowHidden] = useState<boolean>(!props.isOpen);
  const elAccordionRef = useRef<HTMLDivElement | null>(null);
  const elContentRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (elAccordionRef.current == null) {
      return;
    }
    // 処理の最初にoverflow: hiddenをセットするためにisOverflowをtrueにする
    setIsOverflowHidden(true);
    // 現在の高さを設定
    setHeightStyle(`${elAccordionRef.current.clientHeight}px`);

    // setTimeoutを使ってワンテンポ置いて目標の高さに設定する
    setTimeout(() => {
      setHeightStyle(() => {
        if (elContentRef.current == null) {
          return "0px";
        }
        return props.isOpen ? `${elContentRef.current.clientHeight}px` : "0px";
      });
    }, 100);
  }, [props.isOpen]);

  // CSS Transitionが終わった時の処理
  const handleTransitionEnd = () => {
    // 開いた時は高さとoverflowの設定を解除
    if (props.isOpen) {
      setHeightStyle("");
      setIsOverflowHidden(false);
    }
  };

  return (
    <div
      ref={elAccordionRef}
      style={{
        transition: "height 0.5s",
        height: heightStyle,
        overflow: isOverflowHidden ? "hidden" : ""
      }}
      onTransitionEnd={handleTransitionEnd}
    >
      <div ref={elContentRef}>{props.children}</div>
    </div>
  );
};

サンプルコードも置きますので、詳しい実装は以下を参考にしてみてください。

終わりに

以上が React でアコーディオンを自作する場合の方法でした。

弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!

https://www.wantedly.com/projects/1130967

https://www.wantedly.com/projects/768663

Discussion