アコーディオンコンポーネントをReactで自作する
こんにちは!CastingONE の岡本です。
はじめに
弊社のフロントエンド開発は Vue.js を使用していますが、React に移行しようとしています。わたしは React 未経験なので日々勉強しています。React 力を高めるためにコンポーネントを自作することを考えましたが、丁度Vue.js で自作アコーディオンを作成する記事がありましたので、それの React 版を作ってみました。
今回学んだ機能
このコンポーネント作成を通じて学んだ機能を紹介します。
useRef
Vue.js だとテンプレート内の要素を DOM として操作したい場合にはref
属性を指定することで、script 内で$refs
の中で DOM を呼び出すことができました。
React だとuseRef
というフックを使って、current
キーを指定することでで DOM を参照することができます。
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 プロパティの実行が完了したら発生するイベントです。使用方法は以下の通りです。
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
をスタイルに当てて中身が見えないようにします。
// 初期状態(閉じている状態)は0pxを設定する
const [heightStyle, setHeightStyle] = useState<string>(
props.isOpen ? "" : "0px"
);
// 初期状態はoverflow: hiddenを設定する
const [isOverflowHidden, setIsOverflowHidden] = useState<boolean>(!props.isOpen)
この状態で以下の手順を踏んで開くアニメーションをします。
- useRef を使って elContent の高さを取得して、その値をセットする
const [heightStyle, setHeightStyle] = useState<string>(
props.isOpen ? "" : "0px"
);
const elContentRef = useRef<HTMLDivElement | null>(null);
// clientRefを使って要素の高さをセット
setHeightStyle(`${elContentRef.current.clientHeight}px`;);
- CSS Transition で目標の高さの値までアニメーションする
return (
<div
ref={elAccordionRef}
style={{
height: heightStyle,
overflow: isOverflowHidden ? 'hidden' : ''
// CSS Transitionを設定
transition: 'height 0.5s'
}}
>
<div ref={elContentRef}>{props.children}</div>
</div>
)
- onTransitionEnd を使ってアニメーション終了後スタイルをリセットする
スタイルをリセットしないと開いた後にアコーディオンの中身のサイズが変わったとしても、調整されなくなってしまうので、onTransitionEnd
イベントを使って以下の処理を行います。
// 開いた時は高さとoverflowの設定を解除
const handleTransitionEnd = () => {
if (props.isOpen) {
setHeightStyle('');
setIsOverflowHidden(false);
}
};
return {
<div
ref={elAccordionRef}
// ...
// onTransitionEndイベントを使う
onTransitionEnd={handleTransitionEnd}
>
{/* .... */}
</div>
}
閉じる処理
閉じる場合は、以下の手順を踏みます。
- アコーディオン全体の今の高さを取得し、その値をセットする。その後ワンテンポ置いてから 0px をセットする。
useEffect
を使用して、アコーディオン開閉フラグを監視し、フラグが false
の時に0px
をセットします。
ワンテンポ置く理由は、0px をセットしないと、いきなり 0px がセットされる挙動になり、アニメーションが動作しないためです。
useEffect(() => {
// アコーディオンの高さを取得して、セットする
setHeightStyle(`${elAccordionRef.current.clientHeight}px`)
// setTimeoutを使ってワンテンポ置いて0pxをセットする
setTimeout(() => {
setHeightStyle(() => props.isOpen ? `${elContentRef.current.clientHeight}px` : "0px")
}, 100)
}, [props.isOpen])
- CSS Transition で 0px に向かってアニメーションされる
- 閉じた場合はスタイルのリセットは行わない(リセットすると中身が見えてしまうため)
まとめ
開く処理、閉じる処理をまとめると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 でアコーディオンを自作する場合の方法でした。
弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!
Discussion