【初心者】アコーディオンメニューをReactで制作【備忘録】

# はじめに
投稿時点で、筆者は知識ゼロの状態から勉強を初めて2ヶ月程度の実力です。
そのため、理解不足や説明不足、誤った内容や呼び方をしている可能性があります。
万が一参考にする場合は上記の点を考慮した上でご一読ください。

それと今回はNotionに貯めたメモをそのまま貼り付けただけです。
あと結論から言うと、納得のいったものには仕上がりませんでした。
たしか最終的に`React-transition-group`でそれっぽくできたけど、アンサー部分の中身が入れ子になってると閉じてくれなかったはず。

# 制作過程
今回は完成したコードのみしかメモしてませんでした。
コード内には不要なコメントアウトも残ってます。



## CSSの記述
たしかこれだったはず、細かいところは変わってるかも。
上半分は簡易CSSリセット。

```css:Accordion.css
html, body, h1, h2, h3, h4, ul, ol, dl, li, dt, dd, p, div, span, img, a, table, tr, th, td {
  margin: 0;
  padding: 0;
  border: 0;
  font-weight: normal;
  font-size: 100%;
  vertical-align:baseline;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
article, header, footer, aside, figure, figcaption, nav, section { 
  display:block;
}
body {
  line-height: 1;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}
ol, ul {
  list-style: none;
  list-style-type: none;
}






.accordion {
  position: relative;
  top: 5rem;
  left: 5rem;
  width: 20rem;
  text-align: center;
  line-height: 1.5;
}

.item {
  margin-top: 1rem;
}

.menu {
  position: relative;
  height: 3rem;
  font-weight: 600;
  background-color: aqua;
  border-bottom: solid 1px #000;
}

.menu::after {
  content: "＋";
  position: absolute;
  top: 50%;
  right: 1rem;
  transform: translateY(-50%) rotate(90deg);
  transition: transform 0.3s; /* 開閉バッジに関係する */
}

/* 親要素のmenuにopenが付与されたときの動作 */
.open .menu::after {
  content: "－";
  transform: translateY(-50%) rotate(180deg);
}

.contents {
  background-color: aquamarine;
  border-bottom: solid 1px #000;
  line-height: 1.7;
  overflow: hidden;
}

.pointer {
  cursor: pointer;
  user-select: none; /* ダブルクリックした時にテキストが選択されるのが気になるのでそれを消すため */
}

```






## クラスコンポーネントでのやり方

### 親コンポーネント
```javascript:App.js
// クラスコンポーネントでのやり方
import './Accordion.css';
import React from 'react';
import Accordion from './components/Accordion';

class App extends React.Component {
  render() {
    const itemList = [
      {
        menu:'メニュー1',
        contents:'コンテンツ1',
        contents2:'2行目'
      },
      {
        menu:'メニュー2',
        contents:'コンテンツ2',
        contents2:'2行目'
      },
      {
        menu:'メニュー3',
        contents:'コンテンツ3',
        contents2:'2行目'
      }
    ];

    return (
      <div>
        {/* itemListに対するmapメソッド */}
        {itemList.map((item) => {
          return (
            <Accordion
            menu = {item.menu}
            contents = {item.contents} //props名 = propsの値
            contents2 = {item.contents2} //props名 = propsの値
            />
          )
        })}
      </div>
    );
  }
}

export default App;
```


### 子コンポーネント
```javascript:Accordion.js
// クラスコンポーネントでのやり方
import React from 'react';

class Accordion extends React.Component {
  constructor(props) {
    super(props);
    this.state = {open: false}; // 初期値はfalseにして、初期状態では非表示にする
  }

  // clickOpen() { // clickOpenメソッドを定義（開くだけ）
  //   this.setState({open: true}); // stateをtrueにする処理
  // }
  
  // clickClose() { // clickButtonメソッドを定義（閉じるだけ）
  //   this.setState({open: false}); // stateをfalseにする処理
  // }

  toggleButton() { // toggleButtonメソッドを定義（1つのボタンで開閉）
    if(this.state.open === true) {
      this.setState({open: false}); // stateをfalseにする処理
    } else {
    this.setState({open: true}); // stateをtrueにする処理
    }
  } 
  
  render() {
    let modal; // 空の変数を用意
    if (this.state.open) { // openがtrueのときに処理が実行される。これはif(true)のときに実行され、ボタンがクリックされた際には、this.state.openはtrueになるのでif(true)になり実行される仕組み
      modal = (
        <dd className='contents'>{this.props.contents}<p>{this.props.contents2}</p></dd>
      );
    }

    return (
      <dl className='accordion'>
        <div className='item'>
          <dt className='menu pointer' onClick={() => {this.toggleButton()}}>{this.props.menu}</dt>
          {modal} 
        </div>
      </dl>
    );
  }
}


export default Accordion;
```






## 関数コンポーネント（アロー関数）でのやり方

### 親コンポーネント
```javascript:App.js
// 関数コンポーネント（アロー関数）でのやり方
import './Accordion.css';
import Accordion from './components/Accordion';

const App = (props) => {
  const itemList = [
    {
      menu:'メニュー1',
      contents:'コンテンツ1',
      contents2:'2行目'
    },
    {
      menu:'メニュー2',
      contents:'コンテンツ2',
      contents2:'2行目'
    },
    {
      menu:'メニュー3',
      contents:'コンテンツ3',
      contents2:'2行目'
    }
  ];

  return (
    <div>
      {/* itemListに対するmapメソッド */}
      {itemList.map((item) => {
        return (
          <Accordion
          menu = {item.menu}
          contents = {item.contents} //props名 = propsの値
          contents2 = {item.contents2} //props名 = propsの値
          />
        )
      })}
    </div>
  );
}

export default App;
```


### 子コンポーネント
```javascript:Accordion.js
// 関数コンポーネント（アロー関数）でのやり方
import {useState} from 'react';

const Accordion = (props) => {
  // const [現在の状態, 更新関数] = useState(初期値)
  const [open, setOpen] = useState(false); // 初期値はfalseにして、初期状態では非表示にする、useStateに初期値(false)を渡した結果がopen、setOpenになる

  // const clickOpen = () => { // clickOpen関数を定義（開くだけ）
  //   setOpen(true) // setOpen（更新関数）をtrueにする処理
  // }

  // const clickClose = () => { // clickClose関数を定義（閉じるだけ）
  //   setOpen(false) // setOpen（更新関数）をtrueにする処理
  // }

  // const toggleButton = () => { // clickButton関数を定義（1つのボタンで開閉）
  //   if(open === true) {
  //     setOpen(false); // setOpen（更新関数）をfalseにする処理
  //   } else {
  //     setOpen(true); // setOpen（更新関数）をtrueにする処理
  //   } 
  // }

  // 又は（↑と↓どっちでも同じ動き）

  // prevStateはuseStateの更新関数で使える特殊な値
  // precStateは更新前のstateを引数として受け取る
  // 下記だと、変更前(prevState)で受け取った値を、反転（!）して、trueとfalseを反転してreturnしますよって意味
  const toggleButton = () => {
    setOpen(prevState => !prevState);
  }

  let modal; // 空の変数を用意
  if (open) { // openがtrueのときに処理が実行される。これはif(true)のときに実行され、ボタンがクリックされた際には、openはtrueになるのでif(true)になり実行される仕組み
    modal = (
      <dd className='contents'>{props.contents}<p>{props.contents2}</p></dd>
    );
  }

  return (
    <dl className='accordion'>
      <div className='item'>
        {/* <dt className='menu pointer' onClick={clickOpen}>{props.menu}</dt>
        <dt className='menu pointer' onClick={clickClose}>{props.menu}</dt> */}

        <dt className='menu pointer' onClick={toggleButton}>{props.menu}</dt>
        {/* 又は（↑と↓どっちでも同じ動き） */}
        {/* <dt className='menu pointer' onClick={() => toggleButton()}>{props.menu}</dt> */}
        {modal} 
      </div>
    </dl>
  );
}

export default Accordion;
```


## アニメーション部分制作

### React-transition-groupで動きを付ける
これは途中で挫折して失敗に終わりました、その名残です。
たしか、Vanilla JSと同じでアンサー部分に高さ指定しないと開閉アニメーション付けれなかった。
高さ指定せずに開閉アニメーションをしたかったので失敗に終わった。

#### CSS記述
```css:Accordion.css
html, body, h1, h2, h3, h4, ul, ol, dl, li, dt, dd, p, div, span, img, a, table, tr, th, td {
  margin: 0;
  padding: 0;
  border: 0;
  font-weight: normal;
  font-size: 100%;
  vertical-align:baseline;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
article, header, footer, aside, figure, figcaption, nav, section { 
  display:block;
}
body {
  line-height: 1;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}
ol, ul {
  list-style: none;
  list-style-type: none;
}






.accordion {
  position: relative;
  top: 5rem;
  left: 5rem;
  width: 20rem;
  text-align: center;
  line-height: 1.5;
}

.item {
  margin-top: 1rem;
}


.menu {
  position: relative;
  height: 3rem;
  font-weight: 600;
  background-color: aqua;
  border-bottom: solid 1px #000;
}

.menu::after {
  content: "＋";
  position: absolute;
  top: 50%;
  right: 1rem;
  transform: translateY(-50%) rotate(90deg);
  transition: transform 0.3s; /* 開閉バッジに関係する */
}

/* 親要素のmenuにopenが付与されたときの動作 */
.open .menu::after {
  content: "－";
  transform: translateY(-50%) rotate(180deg);
}

.contents {
  background-color: aquamarine;
  border-bottom: solid 1px #000;
  transition: 0.3s; /* アコーディオン開閉に関係する */
  line-height: 2;
}

/* 親要素のmenuにopenが付与されたときの動作 */
/* .open .contents {
} */

.pointer {
  cursor: pointer;
  user-select: none; /* ダブルクリックした時にテキストが選択されるのが気になるのでそれを消すため */
}




.fade-enter {
  opacity: 0;
}

.fade-enter-active {
  opacity: 1;
  transition: opacity 300ms ease-in-out;
}

.fade-exit {
  opacity: 1;
}

.fade-exit-active {
  opacity: 0;
  transition: opacity 300ms ease-in-out;
}
```
#### 子コンポーネント
親コンポーネントの記述は変えてません。
```javascript:Accordion.js
// 関数コンポーネント（アロー関数）でのやり方
import {useState} from 'react';
import { CSSTransition } from 'react-transition-group';

const Accordion = (props) => {
  // const [現在の状態, 更新関数] = useState(初期値)
  const [open, setOpen] = useState(false); // 初期値はfalseにして、初期状態では非表示にする、useStateに初期値(false)を渡した結果がopen、setOpenになる


  // prevStateはuseStateの更新関数で使える特殊な値
  // precStateは更新前のstateを引数として受け取る
  // 下記だと、変更前(prevState)で受け取った値を、反転（!）して、trueとfalseを反転してreturnしますよって意味
  const toggleButton = () => {
    setOpen(prevState => !prevState);
    console.log(open);
  }

  // let modal; // 空の変数を用意
  // if (open) { // openがtrueのときに処理が実行される。これはif(true)のときに実行され、ボタンがクリックされた際には、openはtrueになるのでif(true)になり実行される仕組み
  //   modal = (
  //     <CSSTransition in={open} timeout={300} classNames='fade'>
  //       <dd className='contents'>{props.contents}<p>{props.contents2}</p></dd>
  //     </CSSTransition>
  //   );
  // }

  return (
    <dl className='accordion'>
      <div className='item'>
        {/* <dt className='menu pointer' onClick={clickOpen}>{props.menu}</dt>
        <dt className='menu pointer' onClick={clickClose}>{props.menu}</dt> */}

        <dt className='menu pointer' onClick={toggleButton}>{props.menu}</dt>
        {/* 又は（↑と↓どっちでも同じ動き） */}
        {/* <dt className='menu pointer' onClick={() => toggleButton()}>{props.menu}</dt> */}

        {/* {modal}  */}

        <CSSTransition in={open} timeout={300} classNames='fade'>
          <dd className='contents'>{props.contents}<p>{props.contents2}</p></dd>
        </CSSTransition>

      </div>
    </dl>
  );
}

export default Accordion;
```


### Framer Motionで動きを付ける
ひとまず開閉のみは成功
高さ指定せずに開閉アニメーションをするというのは成功。
`height:auto;`でも機能してくれた。

#### CSS記述
```css:Accordion.css
html, body, h1, h2, h3, h4, ul, ol, dl, li, dt, dd, p, div, span, img, a, table, tr, th, td {
  margin: 0;
  padding: 0;
  border: 0;
  font-weight: normal;
  font-size: 100%;
  vertical-align:baseline;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
article, header, footer, aside, figure, figcaption, nav, section { 
  display:block;
}
body {
  line-height: 1;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}
ol, ul {
  list-style: none;
  list-style-type: none;
}






.accordion {
  position: relative;
  top: 5rem;
  left: 5rem;
  width: 20rem;
  text-align: center;
  line-height: 1.5;
}

.item {
  margin-top: 1rem;
}

.menu {
  position: relative;
  height: 3rem;
  font-weight: 600;
  background-color: aqua;
  border-bottom: solid 1px #000;
}

.menu::after {
  content: "＋";
  position: absolute;
  top: 50%;
  right: 1rem;
  transform: translateY(-50%) rotate(90deg);
  transition: transform 0.3s; /* 開閉バッジに関係する */
}

/* 親要素のmenuにopenが付与されたときの動作 */
.open .menu::after {
  content: "－";
  transform: translateY(-50%) rotate(180deg);
}

.contents {
  background-color: aquamarine;
  border-bottom: solid 1px #000;
  line-height: 1.7;
  overflow: hidden;
}

.pointer {
  cursor: pointer;
  user-select: none; /* ダブルクリックした時にテキストが選択されるのが気になるのでそれを消すため */
}
```
#### 子コンポーネント
親コンポーネントの記述は変えてません。
```javascript:Accordion.js
// 関数コンポーネント（アロー関数）でのやり方
import {useState} from 'react';
import { motion } from 'framer-motion';

const Accordion = (props) => {
  // const [現在の状態, 更新関数] = useState(初期値)
  const [open, setOpen] = useState(false); // 初期値はfalseにして、初期状態では非表示にする、useStateに初期値(false)を渡した結果がopen、setOpenになる

  const variants = {
    opend: {
      height: "auto",
      transition: {
        duration: 0.5,
      },
    },
    closed: {
      height: 0,
      transition: {
        duration: 0.5,
      },
    },
  };

  const toggleButton = () => {
    setOpen(prevState => !prevState);
  }

  return (
    <dl className='accordion'>
      <div className='item'>

        <dt className='menu pointer' onClick={toggleButton}>{props.menu}</dt>

        <motion.dd
          className='contents'
          variants = {variants}
          initial="closed" // 初期状態
          animate= {open ? "opend" : "closed"} //初期状態からこの状態にアニメーションする
        >
          {props.contents}<p>{props.contents2}</p>
        </motion.dd>

      </div>
    </dl>
  );
}

export default Accordion;
```

##### クラス名を付与して疑似要素をCSSで変化させるパターン？
あまり覚えてない、こういうパターンもあるよって感じだったはず。
```javascript:Accordion.js
import {useState} from 'react';
import { motion } from 'framer-motion';

const Accordion = (props) => {
  const [open, setOpen] = useState(false);

  const toggleButton = () => {
    setOpen(prevState => !prevState);
  }

  const variants = {
    opend: {
      height: "auto",
      transition: {
        duration: 0.5, //0.5秒かけて変化する
      },
    },
    closed: {
      height: 0,
      transition: {
        duration: 0.5,
      },
    },
  };

  return (
    <dl className='accordion'>
      <div className={`item ${open ? 'open' : ''}`}> {/* itemというクラス名は残したまま、openがtrueならクラス名にopenを付与、falseならクラス名を削除 */}

        <dt className="menu pointer" onClick={toggleButton}>{props.menu}</dt>

        <motion.dd
          className='contents' // クラス名
          variants = {variants} // const variantsを参照しますよ、その中からinitialの中身を選びますよみたいな
          initial="closed" // 初期状態、const variantsのclosedの値をみにいってる
          animate= {open ? "opend" : "closed"} //初期状態からこの状態にアニメーションする、const variantsのopendの値をみにいってる、openがtrueならopend、falseならclosed
        >
          {props.contents}<p>{props.contents2}</p>
        </motion.dd>

      </div>
    </dl>
  );
}

export default Accordion;
```

# 参考サイト
[【Gatsby製ブログ】framer-motionの基本的な使い方](https://bear-fruit.online/framer-motion-basic/)
[【Gatsby製ブログ】framer-motionでVariantsを使う！](https://bear-fruit.online/framermotion-variants/)
[【Gatsby製ブログ】framer-motionで好きなタイミングでアニメーション発火](https://bear-fruit.online/framermotion-animationstart/)
[【Gatsby製ブログ】framer-motionでスクロールしたらフワッと表示させる](https://bear-fruit.online/framermotion-scrollanimation/)