このプレイヤーの実装方法がお題です。
はじめに
前回、**ReactとAudioの付き合い方**という記事を書きました。
内部状態を持つaudio要素を管理するためにクラスコンポーネントを使って、render propパターンで実装していました。
内部状態を持つ要素を管理するための、もっとわかりやすい、Functionalな方法を求め、
噂のhooksを今回試してみました。
結果的に、内部状態をもつ要素(audio要素だけでなくなんでも当てはまると思う)をhooksで扱うための良い勉強になりました。
ただもっとベターな手段がある気もするので、温かい目でみていただき、アドバイスなどいただけると幸いでございます!
hooksを使ってどう変わったか
**もともと**の登場人物は以下の2つのコンポーネントで、render propパターンを使って作っていました。
-
AudioProvider: audio の内部状態を知ってるし、変更できるけど、audio 要素が画面でどう使われるかは知らないコンポーネント
-
AudioPlayer: Provider から audio の情報(経過時間とか)を受け取って、画面を表示するコンポーネント
今回はReact Hooksを使って、カスタムフックを作りました。
そのおかげで、AudioProviderコンポーネントの役割は、useAudioというカスタムフック(関数)が担うことになりました。
カスタムフックとは(Building Your Own Hooks)
**ソースコード付きのデモ**はこちら。
ここからは実装をみせながら説明します。
AudioPlayer (audio 情報を受け取って見た目を作るコンポーネント)
const AudioPlayer = () => {
const [playing, currentTime, play, pause, jump] = useAudio("path/to/audioUrl");
return (
<>
<p>currenttime: {currentTime}</p>
<button onClick={playing ? pause : play}>
{playing ? "Pause" : "Play"}
</button>
<button onClick={() => jump(30)}>30sec ▶︎</button>
</>
);
};
render propパターンは構造上、初見だと理解しづらいコードになりがちです。比べてhooksはかなりシンプルで直感的。
ここでまず感動しました。
次に、本題のカスタムフックです。
useAudio (audio の内部状態を扱うカスタムフック)
const useAudio = url => {
const [audio] = React.useState(new Audio(url));
const [, _forceUpdate] = React.useState(false);
const forceUpdate = () => _forceUpdate(prevState => !prevState);
React.useEffect(() => {
audio.play();
audio.addEventListener("play", forceUpdate);
audio.addEventListener("pause", forceUpdate);
audio.addEventListener("ended", forceUpdate);
audio.addEventListener("timeupdate", forceUpdate);
return () => {
audio.removeEventListener("play", forceUpdate);
audio.removeEventListener("pause", forceUpdate);
audio.removeEventListener("ended", forceUpdate);
audio.removeEventListener("timeupdate", forceUpdate);
};
}, []);
const play = () => audio.play();
const pause = () => audio.pause();
const jump = value => (audio.currentTime += value);
return [!audio.paused, audio.currentTime, play, pause, jump];
};
ポイントを以下の4つに絞って説明します。
-
- audio要素をなぜuseStateから生成するのか
-
- 自作forceUpdate
-
- useEffectの戻り値
-
- useEffectの第2引数
1. audio要素をなぜuseStateから生成するのか
audio要素を再生成させないため。です。
クラスコンポーネントを使っていた時代に、一度しか生成したくないものをthisに直接生やしていたのと同じ感じです。
2. 自作forceUpdate
クラスコンポーネントを使っていた時代、
内部状態に変更があった時に再描画させるためにはthis.forceUpdateさせるしかありませんでした。
関数ってthisとかないじゃん! なので、
公式では、極力mutableに状態を扱うなという前提だけどもやりようはあるぞ的な感じで紹介しています。
Is there something like forceUpdate?
今回は以下の感じで自作しました。
const [, _forceUpdate] = React.useState(false);
const forceUpdate = () => _forceUpdate(prevState => !prevState);
useState
で初期値に真偽値を渡していて、必ず再レンダリングさせるために直前の真偽値とは逆の値をセットしています。
ちなみにこれはサンプルなので、forceUpdateをuseAudio.jsで再生していますが、実践なら多分汎用化するために別の場所にuseForceUpdate
みたいな名前で定義すると思います。
3. useEffectの戻り値
React.useEffect(() => {
...
return () => {
audio.removeEventListener("play", forceUpdate);
audio.removeEventListener("pause", forceUpdate);
audio.removeEventListener("ended", forceUpdate);
audio.removeEventListener("timeupdate", forceUpdate);
};
}, []);
useEffectメソッドの戻り値は、このフックを使うコンポーネントがアンマウントされた時に呼び出されるそうです。
なので、ここでは登録したイベントハンドラーを一気に解除しています。
クラスコンポーネントの時代にイベントリスナーを使おうとしたら、
componentDidMount
とcomponentWillUnmount
でそれぞれ定義が必要でした。
それに比べてhooksの場合、useEffectの中に一緒に書くことができる!!めちゃくちゃ便利です。
4. useEffectの第2引数
React.useEffect(() => {
...
}, []);
[](空配列)
を第2引数に渡していますが、ここには通常何かしらの値を入れておいて、その値が変更した時にuseEffectの処理を再実行するという挙動になっています。
つまり空配列を渡した場合、常に値が変わらないので、 componentDidMount
の代わりになります。
知識として覚えておくべきは、配列に何も渡さない場合はcomponentDidUpdate
の代わりになります。
(最初これを知らずにaudio.play()を宣言していたので、再レンダリングされるたびに音楽が再生される挙動に悩まされていました..w)
さいごに
まだまだわからないことだらけのhooksですが、少しずつ調べていこうと思います。