ReactとAudioの付き合い方

[![Image from Gyazo](https://i.gyazo.com/661781887cfbd43ee9a62772c609d37f.gif)](https://gyazo.com/661781887cfbd43ee9a62772c609d37f)
---
このプレイヤーの実装方法がお題です。

**[ReactHooksでつくった版](https://qiita.com/kazumicho/items/53572ae56bb70b2e20d0)**はこちら

# はじめに :smiley:

この記事は、React コンポーネントで内部状態をもつ要素を扱うための How を書いたものです。

今回のネタでいうと、内部状態をもつ要素とは、audio 要素のことです。

ここでいう内部状態をもつ要素はどんなものかというと、

```js
const audio = new Audio("sample.mp3") //　コンストラクタでaudio要素を生成

audio.play() // 再生
audio.currentTime = 50 // 経過時間を50秒にする
```

 的なやつです。

内部状態を持っている要素は React の setState で更新する対象にできないし、しないべきです。
（↑ このテーマは  大きくて書ききれないので割愛します）

そうなると出てくる問題として、**audio 要素の状態を直接更新しても、React コンポーネントは再描画されない** んですよね。

はい。今回の問題は上記の通りで、 目指すのは、

**audio 要素の状態を更新すると React コンポーネントが再描画される** です。

そのために今回、**render prop** という React コンポーネントの実装パターンを使います。

# render prop について :tophat:

render prop について今回は詳しく説明しませんが、[以前書いた記事](https://qiita.com/kazumicho/items/4f862b618eda09e14ec7#render-prop%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)で使用例などを交えて説明しています。

簡単に説明すると **「コンポーネントの render メソッドを外部から定義するためのテクニック」** です。

# 本テーマ: 内部状態をもつ要素を React で扱うための How :writing_hand:


まず render prop パターンを使って何がしたいかというと、

audio 要素(内部状態をもつ厄介なやつ)の状態管理と画面描画の責務を分離したいんです。
audio 要素の状態を知ってるヤツには見た目の知識を与えず、その逆もまた然りとする感じです。

なので、実装としては、大きく二つのコンポーネントを用意します。

- **AudioProvider**: audio の内部状態を知ってるし、変更できるけど、audio 要素が画面でどう使われるかは知らないコンポーネント

- **AudioPlayer**: Provider から audio の情報(経過時間とか)を受け取って、画面を表示するコンポーネント

## AudioPlayer (audio 情報を受け取って見た目を作るコンポーネント)

まず、audio 要素の  情報を受け取って画面表示する側の実装をお見せします

今回は冒頭の GIF でお見せした通り、再生・停止と 30 秒ジャンプができるだけのシンプルなプレイヤーを作りました。

```AudioPlayer.jsx
const AudioPlayer = () => (
  <AudioProvider
    url="path/to/audioUrl"
    render={({ currentTime, paused, play, pause, jump }) => (
      <div>
        <p>currenttime: {currentTime}</p>

        <button onClick={paused ? play : pause}>
          {paused ? "Play" : "Pause"}
        </button>

        <button onClick={() => jump(30)}>30sec ▶︎</button>
      </div>
    )}
  />
);
```

`render={({ currentTime, paused, play, pause, jump }) => ...` の部分ですが、

render という名前の props に `引数を受け取ってコンポーネントを返す関数` を渡しています。

実際、このコンポーネントはそんな重要ではないです。 値や関数を受け取って画面に表示してるだけなので。

ただよくみると、受け取っている引数が `currentTime(音声の経過時間)` とか　`play(再生するための関数)` とかですが、これは AudioProvider から渡されます。

それでは本題の AudioProvider 側の実装を  通して、 `引数どっからきてるの？` を  紹介します

## AudioProvider (audio の内部状態を扱うコンポーネント)

```.jsx
class AudioProvider extends React.Component {
  audio = new Audio(this.props.url); // audio要素の生成

  componentDidMount = () => {
    /**
     * audioの内部状態に変化があったときに再描画するための処理
     * ex.) this.playメソッドが実行されると、"play" を登録してるイベントリスナーが反応し、
     *      コンポーネントが強制的に再描画される
     */
    this.audio.addEventListener("play", this.forceUpdate);
    this.audio.addEventListener("pause", this.forceUpdate);
    this.audio.addEventListener("ended", this.forceUpdate);
    this.audio.addEventListener("timeupdate", this.forceUpdate);
  };

  componentWillUnmount = () => {
    this.audio.removeEventListener("play", this.forceUpdate);
    this.audio.removeEventListener("pause", this.forceUpdate);
    this.audio.removeEventListener("ended", this.forceUpdate);
    this.audio.removeEventListener("timeupdate", this.forceUpdate);
  };

  // --------状態変更用のコールバック関数--------
  play = () => this.audio.play();
  pause = () => this.audio.pause();
  jump = value => (this.audio.currentTime += value);

  render = () =>
    this.props.render({
      currentTime: this.audio.currentTime,
      paused: this.audio.paused,
      play: this.play,
      pause: this.pause,
      jump: this.jump
    });
}
```

AudioProvider が

- audio 要素を保持し、
- 変更をゴリゴリ行い、
- 変更を検知して自分自身(とその配下の AudioPlayer コンポーネント)を再描画し、
- 新しい状態を render prop に渡す

などなど、汚れ仕事をたくさんしています。

ここで大事なのは、 `audio要素自体をrender propに渡さない` ということです。

audio 要素の変更がAudioProviderのおかげでブラックボックスにできているのに、
audio要素自体を渡してしまうと、渡された側(AudioPlayer 側)でaudio要素の状態を変えたりできちゃいます。

それを防ぐために、
audio 要素を分解して currentTime や paused などを **ただの値として** render prop に渡しているワケです。

```.jsx
  render = () =>
    this.props.render({
      currentTime: this.audio.currentTime,
      paused: this.audio.paused,
      play: this.play,
      pause: this.pause,
      jump: this.jump
    });
```

# さいごに :airplane:

audio 要素だけではなくて、 **内部状態をもつ要素をReactコンポーネントで綺麗に扱いたい** 場合はこのパターンでだいたい乗り切れる気がします。

ただ、解決策は今回の render prop パターンだけではないと思うので、みなさんのプラクティスも、ぜひ教えて下さい