【React】FramerMotionを利用したアコーディオン
概要
Reactでアコーディオンとなると、コンポーネントライブラリでMUI(https://mui.com/)とかchakura-ui(https://chakra-ui.com/)とあるけど、独自で実装してみたくなったので忘備録としてメモ。
要件
アコーディオンのパターンとして、クリックしたボタンに関連したアコーディオンは開くけど、ほかのボタンをクリックしてもすでに開いてるアコーディオンはまたボタンをクリックしないと閉じない、、というパターンがある。これは連動されていないのでクリックしたボタンがクリックしたかどうかを監視すればいいけど、*他のアコーディオンをクリックしたら開いてるアコーディオンを閉じたいという場合はそうもいかない。今回はそれも実現したいので、以下の条件としてみる。
またせっかくなのでJSONファイルからデータを読み込むことも追加です。
- アコーディオンデータはJSONファイルからデータを取ってくる
- 最初のアコーディオンは開いている
- 他のアコーディオンをクリックししたら開いてるアコーディオンは閉じる
- 開いているアコーディオンをクリックしたらそのアコーディオンが閉じる
実装方法
実装の流れとしてまずは必要なライブラリをインストールする。アニメーションライブラリはいくつかあるけど、今回はFramerMotionを使う。
DEMOはこちら
FramerMotion
まずはFramerMotionをインストール。
npm install framer-motion
インストールしたら以下のように仕様するファイルでimportする。
下記は公式サイトのサンプル。
import { motion } from "framer-motion"
export const MyComponent = ({ isVisible }) => (
<motion.div animate={{ opacity: isVisible ? 1 : 0 }} />
)
公式サイトはこちら
Jsonファイル
今回、外部から読み込むJsonファイルとして、FAQの質問と回答をアコーディオンにしますが以下のJsonファイルを使用する。
{
"faqs": [
{
"question": "誕生日はいつですか?",
"answer": "1990年4月12日生まれです。32歳です。(2022年11月時点)"
},
{
"question": "なぜエンジニアを目指したのですか?",
"answer": "簡単に1,000万円稼げると聞いたので目指しました。"
},
{
"question": "エンジニアをしていて楽しいと感じるときはなんですか?",
"answer": "思っていたようにフロントの実装ができたときです。バックエンドは現在勉強中なのですが、ロジックを考える時間も楽しいです。"
},
{
"question": "これからどうなりたいですか?",
"answer": "まずはフロントエンドを極めていきたいです。バックエンドやデザイン領域にも関心があるので自分で勉強は続けていきたいです。"
},
{
"question": "誕生日はいつですか?",
"answer": "1990年4月12日生まれです。32歳です。(2022年11月時点)"
}
]
}
JSファイル
今回はApp.jsxファイルに全て書いていく。
import { useEffect, useState } from 'react'
import './App.css'
import { motion} from 'framer-motion';
function App() {
{/* 必要なデータ */}
const defaultOpenIndex = 0
const [jsonData, setJsonData] = useState([])
const [openList, setOpenList] = useState([])
{/* jsonファイルを読み込む処理 */}
const readJsonFile = async () => {
{/* jsonファイルをfetchで読み込む */}
const res = await fetch("/dummy.json")
const json = await res.json()
{/* jsonファイルの長さと同じboolean型の配列を作成する */}
setOpenList(() => {
return [...Array(json.faqs.length)].map((v, i) => i === defaultOpenIndex ? true : false)
})
{/* jsonファイルを配列に保存 */}
setJsonData(json.faqs)
}
{/* useEffectでマウント時に処理する */}
useEffect(() => {
readJsonFile()
}, [])
const onClickHandler=(index)=>{
{/* boolean型の配列からクリックしたボタンのインデックス(index)のみ反転させる */}
setOpenList(openList.map((open,i)=> i === index ? !open : false))
}
return (
<div className="App">
<div className="accBlock">
{jsonData.map((faq, index) => {
return (
<div key={index}>
<div onClick={()=> onClickHandler(index)} className={`${"accBlock__btn"} ${openList[index]? "isClicked" :""}`}>{faq.question}</div>
<motion.div key="accordion" animate={{height:openList[index] ? "100%": "0"}} transition={0.3} className="accBlock__body">
<div className="inner">
{faq.answer}
</div>
</motion.div>
</div>
)
})}
</div>
</div>
)
}
export default App
解説
細かい説明をすると長くなるので、ポイントに絞って説明。
必要な変数として以下がある。
-
defaultOpenIndex
:こちらは読み込む時にアコーディオンを開いておきたいインデックス番号 -
jsonData
:読み込んだJSONファイルのデータ。タイトルと内容がある。 -
openList
:boolean型の配列。trueとfalseで管理する。
jsonData
とopenList
はReactHooksのuseState
で管理する。
アコーディオン開閉の状態をboolean型の配列で管理する。
今回の処理で重要なのが、アコーディオンが開いてるかどうかの管理をboolean型の配列openList
で管理してtrue
の場合はアコーディオンが開いていてfalse
の場合は閉じているという状態とする。
またjsonData
の配列のインデックス番号とopenList
のインデックス番号の連動させるので、例えばopenList
の0番目がtrue
だったらjsonData
の0番目のデータのアコーディオンは開いている状態とする。
今回usEffect
でJsonファイルを読み込んでいるが、Jsonファイルと同じ長さのboolean型の配列をスプレッド構文で作成しているが、その際に最初に表示された時に開いておきたいデータに関してはtrue
としたいので、map()
で処理する際にdefaultOpenIndex
とインデックス番号が同じ場合はtrue
にするために三項演算子で処理している。
最初はすべてアコーディオンが閉じたままがいい場合は必要ない。
setOpenList(() => {
return [...Array(json.faqs.length)].map((v, i) => i === defaultOpenIndex ? true : false)
})
map関数でループする際にクリックイベントにインデックス番号を渡す
Reactではだいたい、取得したデータをmap()
でループ処理するが、今回アコーディオンのタイトル部分をクリックしたら開くようにするため、クリックイベントにどのタイトルがクリックされたかインデックス番号(index
)を引数に渡しておく。
{jsonData.map((faq, index) => {
return (
<div key={index}>
<div onClick={()=> onClickHandler(index)} >{faq.question}</div>
{/* ここは省略 */}
</div>
)
})}
boolean型の配列からクリックしたインデックス番号を反転させる
クリックイベントの処理ではクリックした要素のインデックス番号を利用する。
アコーディオン開閉を管理する openList
の配列からクリックした要素のインデックス番号と同じインデックス番号の場合はbooleanの値を反転させる。
const onClickHandler=(index)=>{
{/* boolean型の配列からクリックしたボタンのインデックス(index)のみ反転させる */}
setOpenList(openList.map((open,i)=> i === index ? !open : false))
}
特に下記の処理の部分が大切。
クリックした要素のアコーディオンが開いていたらopen
の値がtrue
からfalse
に、閉じていたらfalse
だったらtrue
になるが、それ以外はfalse
に設定するので、他のアコーディオンをクリックししたら開いてるアコーディオンは閉じるという事が成立する。
!open : false
boolean型の配列のbool値をhtml上に文字列として出力した内容を見ていただくと分かるがクリックすると配列の内容が変わるのが分かります。
開閉するアニメーションの設定
アニメーション自体はFramer Motionを使ってるのでその設定。
animate
のheight
の高さをopenList
の該当するインデックス番号の値を三項演算子で100%
にするか0
にするかを設定することでアコーディオンのアニメーションが動く。
<motion.div key="accordion" animate={{height:openList[index] ? "100%": "0"}} transition={0.3}>
{/* ここは省略 */}
</motion.div>
下記の記事を参考にした。
まとめ
今回、参考にさせていただいた記事です。
Discussion