58
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ReactとAudioの付き合い方

Last updated at Posted at 2019-02-03

Image from Gyazo

このプレイヤーの実装方法がお題です。

**ReactHooksでつくった版**はこちら

はじめに :smiley:

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

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

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

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 について今回は詳しく説明しませんが、以前書いた記事で使用例などを交えて説明しています。

簡単に説明すると 「コンポーネントの 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 パターンだけではないと思うので、みなさんのプラクティスも、ぜひ教えて下さい

58
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?