Context APIを活用して、Reduxのstate管理から外したAccordionコンポーネントを作る

## どんなところでも開いたり閉じたりしたい
Webでは結構おなじみになっているものかと思いますが、「もっと見る」ボタンをポチッと押したら要素がべろーんと出てくるようなものをAccordionなんていいますね。
React+Reduxで作っている場合は、純粋にリストの項目をボーンとStoreにぶん投げてしまえばべろべろーっと出るのでそこまで意識しないかと思います。
ただ、一旦デザイン的には要素を閉じておくけど、 別に通信するわけでないのでもともと静的に持っているDOMをべろんと出したいなんてこともよくありますよね？

「いやいや、こんなアコーディオンが開いてるとか閉じてるとか、そんなことのためにわざわざAction作りたくないねん。」
「アコーディオンが開いてることをStoreが知ってなんの得があんねん。」

と思う方多いのではないでしょうか？

私は思いました！
かなり必死に調べたので、せっかくなので記しておきたいと思います。

## stateやreducerの肥大化の闇
[ReduxにおけるGlobal stateとLocal stateの共存](http://made.livesense.co.jp/entry/2016/09/29/080000)
こちらの記事に、Global stateとLocal stateを共存して書くことによって、肥大化が起こりにくくなるかもね〜ということが書いてあります。
もちろんケースバイケースだと思いますが、アコーディオンなんかは開いているか否かなんてのをLocal stateに任せても別にいいじゃんねー？と私は思うわけです。
（ただし、必ず一つのアコーディオンしか開かないというような、他のアコーディオンと連携しないことを想定しています。アコーディオン同士が干渉する場合は別です。）

というわけで、ポチッと押したらべろんと出てくる超汎用的なコンポーネントを作ってみましょう。

## やりたいことのまとめ
まず、どういうものを作りたいのかをちゃんと整理しておきましょう。

- Accordionの中にはすべての要素を受け付けられるようにする
- Accordionの中身の中で、指定した箇所だけのDOMが消えたり表示されたりするようにする
- トリガーとなるボタンは、Accordionの子となるか、孫となるか、ひ孫となるか、さらにそれ以下の要素になるかは不明である（デザインによってDOM構造が変わるので当然ですよね）
- トリガーとなるボタンは好きな要素を入れられるようにする（テキストリンクなのか、buttonタグなのか選べた方がいいですよね）
- Accordion機能を追加するためだけに新たなDOMは追加しない（CSSの隣接セレクタとか使えなくなるのいやですよね）

ひえええという感じですが、汎用性を出すためなので頑張りましょう！
ただ、上記のリストだけではどういう風に書きたいのか一向にイメージがつかないと思いますので、上記を叶えた仕組みを使った上で、どうコーディングをしようとしているのかを書いておきたいと思います。

```react:App.jsx
import react from 'react';
import Accordion, { AccordionArea, AccordionToggleButton } from './Components/Accordion';

const App = () => (
  <Accordion>
    <p>
      <AccordionToggleButton>
        <button>開く</button>
      </AccordionToggleButton>
    </p>
    <AccordionArea>
      <p>アコーディオンの中身だよ</p>
    </AccordionArea>
  </Accordion>
)

export default App;
```

このように書けるといいな！ということです！
今回、わざと`Accordion`と`AccordionToggleButton`の間に`<p>`を挟んでいまして、必ずしも子、孫、ひ孫、さらにそれ以下の要素になっても良いというのを意識付けるためにこうしました。（AccordionAreaも然りですが、冗長化するなと思って直下に置いています。ただ、要素の位置には依存させないようにします。）
※`Accordion`のimport元が`'./Components/Accordion'`とかになってしますが、特に深い意味はないので、好きな位置で大丈夫です！

さらに、これが開いた状態の時に、通常のDOMツリー状はどういう風になるかというと、、、

```html
<p>
  <button>閉じる</button>
</p>
<p>アコーディオンの中身だよ</p>
```

という形になります。
jQueryとかで実装する場合は、良くアコーディオンする場所をさらにもう一つ`div`とかで囲っていたと思いますが、今回の場合は囲っても囲わなくてもどちらでもOKというのが実現できるようになります。便利！

普通に便利そうなので、これを実現するためのAccordionコンポーネント群を作ってみたいと思います！
※注意：このコンポーネントはアニメーションは想定していません！ごめんなさい！

## 肝はContext APIとcloneElement
今回のように、

- どの深さにどんな要素が来るかわからない
- 自身の子に親からpropsを渡したい

というケースには、それぞれ、**Context API**と**cloneElement**の二つが解決してくれます。
※注意：`Context API`は`v16.3`以降らしいです！`react`と`react-dom`のバージョンアップを必ずしてください！私は動かない理由が小一時間わからなくて辛かったです！

### Context API
これはreactが用意してくれているもので、ルートとなるコンポーネントのstateをバケツリレーせずに任意の場所から提供できるようになるという代物です。

reduxをやったことある人は、まさしくreduxだー！と思うはずです。
react-reduxを使って任意のコンポーネントをcontainerでconnectすりゃいいというのとかなり似ています。

ただし、reduxと大きく違うのは、Local stateとして扱うところです。小さなstoreが生まれるイメージです。reduxだけでやっていた人には少々違和感があるかもしれませんが、今回はreduxを肥大化させないようにLocal stateを活用したいと思います！

以下の記事を参考に作ってみます。
[React context APIを触ってみた](http://rskull.hateblo.jp/entry/2018/03/02/021753)

まずはふつうにコンポーネントを作っていきます。
全体をラップする`Accordion`コンポーネントと、出たり閉じたりする箇所となる`AccordionArea`の2種類を作ります。

```react:Accordion.jsx
import React, { Component } from 'react';

export default class Accordion extends Component {
  constructor(props) {
    super(props);
    this.props = props;

    this.state = {
      isExpanded: false,
    };
  }

  render() {
    return this.props.children;
  }
}
```

```react:AccordionArea.jsx
import React from 'react';

const AccordionArea = ({ children }) => (
  children
);

export default AccordionArea;
```

とてつもなく無意味な2コンポーネントが出来上がりましたね＾＾
ただただ、子要素を返すだけの、なんのために生まれたのかわからないコンポーネントが出来上がりました。

本来やりたいことは、`Accordion`で作ったstateをうまいこと`AccordionArea`に引き渡して、`isExpanded`を活用して`AccordingArea`内の`children`を出したり閉まったりすればいいはずなのですが、当然`isExpanded`は`Accordion`でしか使うことができません。

なら、`Accordion`から子の`AccordionArea`に渡せばいいじゃんって話ですが、今回は**`AccordionArea`が必ず子に来るとは限らず、孫かもしれないし、ひ孫かもしれない**という状況なのでそれもできません。

**そこで、Context APIの登場です！**

まず、以下のようなファイルを作ります。

```react:AccordionContext.jsx
import { createContext } from 'react';

const AccordionContext = createContext();

export default AccordionContext;
```

これだけです。
このファイルを、先ほど作った各コンポーネントにimportして活用していきます。
まずは何も説明しないまま、思うがままにContext APIを使ったコードへと修正していきます。すみません。
後ほどちゃんと説明します。

```react:Accordion.jsx
import React, { Component } from 'react';
import AccordionContext from './AccordionContext';

export default class Accordion extends Component {
  constructor(props) {
    super();
    this.props = props;

    this.state = {
      isExpanded: false,
    };
  }

  render() {
    return (
      <AccordionContext.Provider value={{
        state: this.state,
        actions: {
          toggle: (state) => {
            this.setState({
              isExpanded: !state,
            });
          },
        },
      }}
      >
        {this.props.children}
      </AccordionContext.Provider>
    );
  }
}
```

```react:AccordionArea.jsx
import React from 'react';
import AccordionContext from './AccordionContext';

const AccordionArea = ({ children }) => (
  <AccordionContext.Consumer>
    {({ state }) => {
      if (state.isExpanded) {
        return children;
      }

      return null;
    }}
  </AccordionContext.Consumer>
);

export default AccordionArea;
```

ここで肝になってくるのは`AccordionContext.Provider`と`AccordionContext.Consumer`の2点です。

この二つは、AccordionContext.jsxで生成したものを各コンポーネントで使っているということなので、つまり`createContext`によって生み出されたものということになります。

ProviderもConsumerも、どちらもReactコンポーネントになるので`<Provider>`、`<Consumer>`と書けるわけです。
ちなみにですが、今回、私が`AccordionContext.Provider`と書いているのは、ただ単に変数に入れるのがめんどくさかっただけです＾＾

この`Provider`と`Consumer`は何ができるのかをざっくりと説明すると、

**Providerに渡したvalueをConsumerで受け取れることができる**

というわけです！
※詳しくは[こちら](https://qiita.com/loverails/items/50126e874b24ff984471)を参照してください。

Accordion.jsxのrenderで`<AccordionContext.Provider value={}>`の中に、少し大きめのobjectを渡しています。

```react

{
  state: this.state,
  actions: {
    toggle: (state) => {
      this.setState({
        isExpanded: !state,
      });
    },
  },
}
```

切り出してみました。
オブジェクトの中身は特に型とか決まっていなくて、好きな形で良いと思うのですが、一番重要なポイントは**`Consumer`で受け取りたいものをここで定義しておく**ということです。

今回は、`Consumer`に対して渡したいものは、**`Accordion`で管理している`this.state`**と、**ボタンの`onClick`で使う、共通のメソッド群**だと思います。

この2つを、ただ好きに`Provider`から渡せばいいのですが、ちょっとだけreduxと[先ほど紹介したこちらの記事](http://rskull.hateblo.jp/entry/2018/03/02/021753)にインスパイアされて、`state`と`actions`というシャレたプロパティ名をつけてみたという感じです＾＾

今度は`AccordionContext.Consumer`の方ですが、こちらは少々書き方が独特です。

```react
const AccordionArea = props => (
  <AccordionContext.Consumer>
    {({ state }) => {
      if (state.isExpanded) {
        return props.chidlren;
      }

      return null;
    }}
  </AccordionContext.Consumer>
);
```

切り出してみました。
`Consumer`のあとにいきなり関数を入れていますが、この関数の引数に、`Provider`の`value`で投げたオブジェクトが流れてくるわけです。
なので、この関数を省略するとせっかく`Provider`が流してくれた値を活用することができないので、絶対に入れるようにしましょう。お約束ってやつですね。
`AccordionArea`では`actions`は特に必要ないので、`{ state }`と記述して`state`だけを切り出し、`if`で純粋に出し分けを行っているだけです。
ロジック自体はとてもシンプルですね。

こうすることで、要素をどれだけネストしても、期待通りに値を受け渡すことができました！

さて、せっかくactionsの方も作ったので、`Consumer`を活用して`AccordionToggleButton`も作ってみましょう。

```react:AccordionToggleButton.jsx
import React from 'react';
import AccordionContext from './AccordionContext';

const AccordionToggleButton = ({ children }) => (
  <AccordionContext.Consumer>
    {({ state, actions }) => (
      children
    )}
  </AccordionContext.Consumer>
);

export default AccordionToggleButton;
```

困りました。。。何が困ったか察しの良い方ならわかるかと思います。。。

**actionsの関数を付与する隙がねぇ！**

「クリックしたら」というのは、**`children`に入ってくる要素の「onClick」に関数を渡さなければならない**のに、渡し方が全然わからない！！

ただ、さっき「やりたいことまとめ」でこんなことを書きました。
> トリガーとなるボタンは好きな要素を入れられるようにする（テキストリンクなのか、buttonタグなのか選べた方がいいですよね）

*「選べたほうが良いですよね」*じゃねぇよって話ですが、まぁ確かに選べた方がいいので、これをなんとかしていきます。

### cloneElement
ここでやっと**`cloneElement`**が出てきます。
これを使うことによって、

**`children`に`props`を追加させることができちゃいます！**

本当にそんなことやっていいのかどうかはちょっと心苦しいですが、せっかくなのでやってみましょう。

今回やりたいのは、

**`Accordion`からもらってきた`actions`を、`AccordionToggleButton`の中にある`children`の`onClick`に付与する。**

ということです。わかりづらい。。

要するに

```js
this.props.children.props.onClick = actions.toggle;
```

とやりたいわけです。
ただこんなことしたらReactがエラー吐きまくる（propsの上書きは基本的には禁止）ので、それを解決するのが`cloneElement`というわけです。

先程の`AccordionToggleButton`を改良してみました。
※以下の記事を参考にしています。
[React で this.props.children に新しい Props を渡す](http://blog.mudatobunka.org/entry/2016/08/14/182333)

```react:AccordionToggleButton.jsx
import React, { Children, cloneElement } from 'react';
import AccordionContext from './AccordionContext';

const AccordionToggleButton = ({ children }) => {
  const childrenWithProps = (_children, onClick) => (
    Children.map(_children, child => (
      cloneElement(child, { onClick })
    ))
  );

  return (
    <AccordionContext.Consumer>
      {({ state, actions }) => (
        childrenWithProps(children, () => (actions.toggle(state.isExpanded)))
      )}
    </AccordionContext.Consumer>
  );
};

export default AccordionToggleButton;

```

だいぶ変えちゃいましたが、大きく変えたのは`childrenWithProps`という関数を作ったことです。
この関数で`Children`と`cloneElement`を使うためにReactから同時にimportしています。
この`Children`というのは、私もよく知りませんが、`this.props.children`を処理しやすいようにreactが用意してくれているものなんだとか。
今回はその中の`map()`を使いました。`Array.prototype.map()`と大差ないと思います。

`cloneElement`は、第一引数にReactElementを入れて、第二引数に`props`となるオブジェクトを入れると、新たに要素を返してくれる関数です。
`Children.map`で一つ一つに分解された`child`を`cloneElement`で`{ onClick }`を付与しながらクローンして、それをすべて返すのが`childrenWithProps`のお仕事となるわけです。

`childrenWithProps`の第二引数であえて`onClick`という名前で引き受けることで、`cloneElement`の第二引数に`{ onClick }`と書けば良くなります。

なんでそもそも、`onClick`という名前で渡しているかというと、reactは`<div onClick={() => { // なにか処理 }}>`と書くと、クリックしたときの処理を行うことができるので、今回の処理は、強制的に子要素に`onClick`の処理を追加してね！という処理になります。
※ここはちょっと賛否両論あるかなと思っています。

さらに、もう一つ肝となるのは

```js
childrenWithProps(children, () => (actions.toggle(state.isExpanded)))
```

の箇所なのですが、さらに

```js
() => (actions.toggle(state.isExpanded))
```

のところです。
ここ、ちょっと違和感がありませんか？

```js
actions.toggle(state.isExpanded)
```

なぜこれではいけないのか？

これ、結構JSを書いていてハマりがちで、実はFlashの時代からこの問題はあります。
もちろん、`onClick`には関数を渡さなければなりませんが、もし、、

```js
actions.toggle(state.isExpanded)
```

を渡してしまうと、これはもう実行された結果が渡されることになるので、`actions.toggle()`が返した値が`onClick`に渡ってしまうことになります。
なので、今回は特に返り値を設定していないので`undefined`ということになります。やばい。

なので、、、

```js
() => (actions.toggle(state.isExpanded))
```

こうしてあげることで、**関数を返す関数**を作ってあげるのです。
es5で書いたほうがわかりやすいと思うので、上記の式をes5に書き換えてみます。

```js
function() {
  return actions.toggle(state.isExpanded)
}
```

こうしてあげれば、`onClick`に渡るときにはちゃんと`actions.toggle(state.isExpanded)`が渡るようになります。
reactになると結構しれっと書かれていることが多いので、お気をつけて〜〜
※Flash AS3を題材に同じようなことが書かれているのはこちら→[addEventListenerで一緒に引数を渡したい](http://blog.img8.com/archives/2008/04/003758.html)

## 仕上げ
そんなこんなで各プログラムが書けたので、ここでAccordion.jsxにちょっと小細工をいれます。

```react:Accordion.jsx
import React, { Component } from 'react';
import AccordionContext from './AccordionContext';
import AccordionArea from './AccordionArea';
import AccordionToggleButton from './AccordionToggleButton';

class Accordion extends Component {
  constructor(props) {
    super();
    this.props = props;

    this.state = {
      isExpanded: false,
    };
  }

  render() {
    return (
      <AccordionContext.Provider value={{
        state: this.state,
        actions: {
          toggle: (state) => {
            this.setState({
              isExpanded: !state,
            });
          },
        },
      }}
      >
        {this.props.children}
      </AccordionContext.Provider>
    );
  }
}

export {
  Accordion as default,
  AccordionArea,
  AccordionToggleButton,
}
```

`AccordiontArea`と`AccordionToggleButton`を`Accordion`に集約させてあげました！
こうしてあげると、いざ使おうというときに

```react
import Accordion, { AccordionArea, AccordionToggleButton } from './Components/Accordion';
```

というふうに書くことができます！（実は冒頭の`App.jsx`でもこういう記述をしていました）

これで晴れて、以下の記述ができるようになりました！

```react:App.jsx
import react from 'react';
import Accordion, { AccordionArea, AccordionToggleButton } from './Components/Accordion';

const App = () => (
  <Accordion>
    <p>
      <AccordionToggleButton>
        <button>開く</button>
      </AccordionToggleButton>
    </p>
    <AccordionArea>
      <p>アコーディオンの中身だよ</p>
    </AccordionArea>
  </Accordion>
)

export default App;
```

めでたし！

## あとがき
ずっと、CSSとか初級編とかばっかり書いていたので、ようやくReactの記事が書けて楽しかったです！
この辺の知識はまだ覚えたてなので、間違いとかあればコメントください！