ReactとAudioの付き合い方
ReactとAudioの付き合い方:
このプレイヤーの実装方法がお題です。
この記事は、React コンポーネントで内部状態をもつ要素を扱うための How を書いたものです。
今回のネタでいうと、内部状態をもつ要素とは、audio 要素のことです。
ここでいう内部状態をもつ要素はどんなものかというと、
的なやつです。
内部状態を持っている要素は React の setState で更新する対象にできないし、しないべきです。
(↑ このテーマは 大きくて書ききれないので割愛します)
そうなると出てくる問題として、audio 要素の状態を直接更新しても、React コンポーネントは再描画されない んですよね。
はい。今回の問題は上記の通りで、 目指すのは、
audio 要素の状態を更新すると React コンポーネントが再描画される です。
そのために今回、render prop という React コンポーネントの実装パターンを使います。
render prop について今回は詳しく説明しませんが、以前書いた記事で使用例などを交えて説明しています。
簡単に説明すると 「コンポーネントの render メソッドを外部から定義するためのテクニック」 です。
まず render prop パターンを使って何がしたいかというと、
audio 要素(内部状態をもつ厄介なやつ)の状態管理と画面描画の責務を分離したいんです。
audio 要素の状態を知ってるヤツには見た目の知識を与えず、その逆もまた然りとする感じです。
なので、実装としては、大きく二つのコンポーネントを用意します。
まず、audio 要素の 情報を受け取って画面表示する側の実装をお見せします
今回は冒頭の GIF でお見せした通り、再生・停止と 30 秒ジャンプができるだけのシンプルなプレイヤーを作りました。
render という名前の props に
実際、このコンポーネントはそんな重要ではないです。 値や関数を受け取って画面に表示してるだけなので。
ただよくみると、受け取っている引数が
それでは本題の AudioProvider 側の実装を 通して、
AudioProvider が
ここで大事なのは、
audio 要素の変更がAudioProviderのおかげでブラックボックスにできているのに、
audio要素自体を渡してしまうと、渡された側(AudioPlayer 側)でaudio要素の状態を変えたりできちゃいます。
それを防ぐために、
audio 要素を分解して currentTime や paused などを ただの値として render prop に渡しているワケです。
audio 要素だけではなくて、 内部状態をもつ要素をReactコンポーネントで綺麗に扱いたい 場合はこのパターンでだいたい乗り切れる気がします。
ただ、解決策は今回の render prop パターンだけではないと思うので、みなさんのプラクティスも、ぜひ教えて下さい
はじめに
この記事は、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 について
render prop について今回は詳しく説明しませんが、以前書いた記事で使用例などを交えて説明しています。簡単に説明すると 「コンポーネントの render メソッドを外部から定義するためのテクニック」 です。
本テーマ: 内部状態をもつ要素を React で扱うための How
まず 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 }); }
- 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 });
さいごに
audio 要素だけではなくて、 内部状態をもつ要素をReactコンポーネントで綺麗に扱いたい 場合はこのパターンでだいたい乗り切れる気がします。ただ、解決策は今回の render prop パターンだけではないと思うので、みなさんのプラクティスも、ぜひ教えて下さい
コメント
コメントを投稿