🗂

【React】FramerMotionを利用したアコーディオン

2023/01/07に公開

概要

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 }} />
)

公式サイトはこちら
https://www.npmjs.com/package/framer-motion

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ファイルに全て書いていく。

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で管理する。

jsonDataopenListは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を使ってるのでその設定。
animateheightの高さをopenListの該当するインデックス番号の値を三項演算子で100%にするか0にするかを設定することでアコーディオンのアニメーションが動く。

<motion.div key="accordion" animate={{height:openList[index] ? "100%": "0"}} transition={0.3}>
  {/* ここは省略 */}
</motion.div>

下記の記事を参考にした。
https://www.npmjs.com/package/framer-motion

まとめ

今回、参考にさせていただいた記事です。
https://teratail.com/questions/366941?sort=1

Discussion