ReactとAudioの付き合い方

ReactとAudioの付き合い方:


Image from Gyazo

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


はじめに :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 の内部状態を扱うコンポーネント)

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 に渡しているワケです。

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 パターンだけではないと思うので、みなさんのプラクティスも、ぜひ教えて下さい

コメント

このブログの人気の投稿

投稿時間:2021-06-17 22:08:45 RSSフィード2021-06-17 22:00 分まとめ(2089件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)