Dave Ceddia氏による全5回におよぶReact Hooks入門記事の第1回を本人の許可を得て意訳しました。
誤りやより良い表現などがあればご指摘頂けると助かります。
原文: https://daveceddia.com/intro-to-hooks/
先日、ReactチームはHooksと呼ばれる大きな新機能をリリースしました。
興奮を抑えきれないのですが、落ち着いて全体を見ていきましょう。
Hooksとは?
ご存知の通り、Reactのクラスコンポーネントはstateを保持することができ、ファンクションコンポーネントにはそれができません。
また、クラスコンポーネントはライフサイクルを持ち、ファンクションコンポーネントはそれを持ちません。
クラスコンポーネントは PureComponent
を継承できますが、ファンクションコンポーネントは都度レンダリングされます。
ところが、Hooksが全てを変えます。(厳密には、最後の一点はv16.6の新機能 React.memo
によって実現されます)
Hooksはファンクションコンポーネントにstateを追加したり、 componentDidMount
や componentDidUpdate
のようなライフサイクルメソッドをフックさせることができます。
これからは、ファンクションコンポーネントを書き、後からstateが少し必要になったりしてもクラスに書き換える必要は一切ありません。これらはもう「ステートレス・ファンクショナル・コンポーネント(SFC)」になることはありません。
メモ:Hooksは現在α版であり、プロダクション環境ではまだ使用できません。APIはさらに変更される可能性があるため、現時点ではプロダクションアプリの書き換えはオススメしません。Open RFCにコメントし、公式ドキュメントやFAQにも目を通してください。
扱い慣れたクラスはどうなる?
Hooksはクラスを置き換えるものではありません。必要に応じて使える新たなツールに過ぎません。
Reactチームは、Reactでクラスを非推奨にすることはないと明言しているため、そのまま使い続けたければそれでも全く問題ありません!
学ばなければならない新しいことが常にあるように感じるのは大変な苦痛です。(私自身も間違いなくそう感じ、このブログを書いています!)Hooksは必要に応じて使える素敵な新機能であり、新たな銀の弾丸というわけではありません。古いコードをHooksを使って書き換えるつもりはありませんし、Reactチームもまた同様の見解を持っています。
いつもの書き方で始める
実際にHooksの例を見てみましょう。皆さん良くご存知のファンクションコンポーネントから始めます。
OneTimeButton
はまだその名前に見合う機能を持っていません。クリック時に onClick
関数を呼ぶだけです。続けて実装していきましょう。
import React from 'react';
import { render } from 'react-dom';
function OneTimeButton(props) {
return (
<button onClick={props.onClick}>
You Can Only Click Me Once
</button>
);
}
function sayHi() {
console.log('yo');
}
render(
<OneTimeButton onClick={sayHi}/>,
document.querySelector('#root')
);
このコンポーネントで実現したいのは、それがクリック済みであるかどうかを保存し、クリック済みの場合はボタンを使用不可にすることです。いわゆるワンタイムスイッチのようなものです。
しかしstateが必要です。関数はstateを保持できないので、クラスに変換する必要がありそうです。
そのようにして、関数をクラスに変換する5ステージが開始されます。
- 拒絶。クラスにする必要はないかも。どこか他の場所にstateを保持できるかも。
- 認識。うーん、クラスにしなきゃダメかな?
- 受容。あー、もう変換するしかない。
-
苦行。
React.Component
を継承したクラスを書き、render
の中に関数の本体をコピペします。おっと、インデントが間違っています。render関数を選択し、タブタブタブ、シフトタブ。そこまでしてもコンポーネントはまったく同じことをしています。(何だかんだで運が良ければ) - 到達。ここでようやくstateが追加。
それでは、関数をクラスに変換してみましょう。
class OneTimeButton extends React.Component {
// stateの初期化...
state = {
clicked: false
}
// クリックハンドラの作成
handleClick = () => {
// ハンドラはボタンが使用不可であれば呼ばれないため、
// ここでクリックを発火しても安心です。
this.props.onClick();
// OK。もうクリックする必要はありません。
this.setState({ clicked: true });
}
render() {
return (
<button
onClick={this.handleClick}
disabled={this.state.clicked}
>
You Can Only Click Me Once
</button>
);
}
}
これはコードのほんの一部ですが、コンポーネント構造に大きな変更があります。小さな変更や並べ替えがたくさん必要です。現時点では非公式であるJavaScriptのクラスプロパティ(Babelプラグインとしてはサポートされていますが)のようなショートハンドも使われています。
Hooksで手軽にstateを追加する
そこで、新しいuseStateフックを使ってプレーンなファンクションコンポーネントにstateを追加してみましょう。
// useStateフックをimportする必要があります:
// (またはReact.useStateを書きます)
import React, { useState } from 'react';
function OneTimeButton(props) {
// 新しいstateを作成します。
// 独自の更新関数が付属しています!
const [clicked, setClicked] = useState(false);
// ボタンクリックをコールバックprop呼び出しと
// ボタン無効化で処理する必要があります。
function doClick() {
props.onClick();
setClicked(true);
}
// このパートはほぼ同じですが、
// thisがない分だけすっきりしています。
return (
<button
onClick={clicked ? undefined : doClick}
disabled={clicked}
>
You Can Only Click Me Once
</button>
);
}
コードの仕組み
コードの大部分は、直前に書いたプレーンなファンクションコンポーネントのままです。 useState
周りを除いては。
useState
はフックです。その名前が「use」で始まる(Hooksのルール - 名前が「use」で始まらなければならない)からです。
useState
フックは初期stateを引数として取り(今回は false
)、2つの要素を持った配列を返します。現在のstateとそのstateを更新する関数です。
クラスコンポーネントは1つの大きなstateオブジェクトを持ち、 this.setState
関数で一度にまとめて更新(加えて新しい値をシャローマージする)します。
ファンクションコンポーネントは全くstateを持ちませんが、 useState
フックにより、stateの小さな塊を必要に応じて追加することができます。単一のブール値が必要なだけであれば、それを保持するための小さなstateを作ることができます。
これらのstateの小片はアドホックな方法で作成しているため、コンポーネント全体の setState
関数はなく、各stateごとの更新関数が必要なのは理にかなっています。つまり、値と関数のペアです。JSの型であれば、number、boolean、object、arrayなど何でも構いません。
ここで、下記のような多くの疑問が湧いてくるでしょう。
- コンポーネントはいつ再レンダリングされるのか...stateは毎回作成されないのか?Reactはどうやって古いstateを知るのか?
- なぜ名前が「use」で始まらなければいけないのか?ちょっと疑わしい。
- ネーミングルールがあるということは、自分自身のフックも作れるということ?
- より複雑なstateはどうやって保存する?複数の値を保存したい。
- Hooksの使い方を今すぐ教えて!
それらについてお話しましょう。
Hooksを使える場所
Hooksをさっそく使ってみたいのであれば、React v16.7.0-alphaビルド(もしくはそれ以降)を使う必要があります。ローカルプロジェクトで試すのであれば、 package.json
を開いて react
と react-dom
のバージョンを "16.7.0-alpha"
に更新してください。念のためお伝えしておくと、「alpha」は「プロダクション用ではない」ということです。
もしくは、Hooksが利用可能なCodeSandboxを使っても構いません。
Hooksの魔法
ああ、ステートレスに見えるファンクションコンポーネントにステートフルな情報を保存するのは奇妙な矛盾に思えるかもしれません。かくいう私自身もこれがHooksに抱いた初めての疑問であり、どのように機能しているのかを理解する必要がありました。
最初の予測では、コンパイラがトリッキーな動作をしているのではないかと考えました。 useWatever
を検索して何とかしてステートフルなロジックに置換しているのではないかと。
続いて、呼び出し順のルールを知ってますます混乱しました。実際にどのように機能するかを見ていきましょう。
Reactが初めてファンクションコンポーネントをレンダリングする時、グローバルではない、そのコンポーネントインスタンスのための別個のオブジェクトを作成します。このコンポーネントのオブジェクトは、コンポーネントがDOMに存在する限り存続します。
Reactはそのオブジェクトを使って、コンポーネントに属する様々なメタデータを記録することができます。
そこには、空配列から始まるフックの配列が存在し、フックを呼ぶたびにReactはその配列に項目を追加します。
呼び出し順が重要な理由
このようなコンポーネントがあるとしましょう。
function AudioPlayer() {
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);
// < 美しいオーディオプレイヤーがここに入ります >
}
useState
が3回呼ばれるので、Reactは最初のレンダリングで3つのエントリをフックの配列に追加します。
次のレンダリング時に、これら3つのフックは同じ順序(当然)で呼び出されるため、Reactは配列の中身を見て、「ああ、0ポジションに useState
フックを既に持っているから、新たなstateを作成する代わりに既存のstateを返そう」となる。
複数のuseState呼び出しをステップバイステップで理解する
どのように機能するのかをより詳細に見ていきましょう。最初のレンダリングは次の通りです。
- Reactがコンポーネントを作成しました。関数はまだ呼び出されていませんが、それはメタデータオブジェクトとフックの空配列を作成します。実行される最初のフックはポジション0を消費します。
- Reactはコンポーネント(フックを保存するメタデータオブジェクトを知っている)を呼び出します。
-
useState
を呼び出します。Reactは新しいstateの小片を作成し、フック配列のポジション0に設定し、volume
の初期値を80
として[volume, setVolume]
のペアを返します。またnextHook
のインデックスを1にインクリメントします -
useState
を再度呼び出します。Reactは配列のポジション1を見て、空であることを確認すると、stateの新たな小片を作成します。そしてnextHook
のインデックスを2にインクリメントして[position, setPosition]
を返します。 -
useState
の三度目の呼び出しです。Reactはポジション2が空であることを確認し、stateを作成してnextHook
のインデックスを3にインクリメントし、[isPlaying, setPlaying]
を返します。
フックの配列には3つの項目が含まれ、レンダリングが終了しました。次のレンダリングでは何が起こるのでしょうか?
- Reactはコンポーネントの再レンダリングを必要とします。こんにちは古き友よ。Reactはこのコンポーネントを知っているので、メタデータは既に関連付けられています。
- Reactは
nextHook
のインデックスを0にリセットして、コンポーネントを呼び出します。 -
useState
を呼び出します。Reactはフック配列のインデックス0を見て、スロットに既にフックが含まれていることを確認します!新たに作成する必要はありません。nextHook
のインデックスを1にインクリメントしてvolume
をまた80にセットして[volume, setVolume]
のペアを返します。 -
useState
を再度呼び出します。今回、nextHook
は1なので、Reactは配列のインデックスを確認します。再度、フックは既に存在しているので、nextHook
をインクリメントして[position. setPosition]
を返します。 -
useState
の三度目の呼び出しです。もう何が起こるか分かりますね?
以上です。決して魔法などではなく、いくつかの真実に基づいています。これはいくつかのHooksルールに繋がります
Hooksのルール
ファイトクラブではありませんが、従うべきいくつかのルールがあります。
- 関数のトップレベル階層でのみフックを呼び出します。ループ文、条件文やネストされた関数内で呼んではいけません。Reactがフックを記録できるように、同じものは同じ順序で呼び出す必要があります。
- Reactのファンクションコンポーネントかカスタムフックからのみ呼び出します。コンポーネントの外部から呼び出してはいけません。全ての呼び出しをコンポーネントもしくはカスタムフック内部で行うことで、関連ロジックがまとめてグルーピングされるため、コードが把握しやすくなります。
- フックの名前は「use」で始めなければいけません。
useState
やuseEffect
(まぁ、これらは予約済みなので使えませんが)のような感じです。
カスタムフック
ネーミングルールがあるということは、自分自身のフックを作れるのではないかと思うかもしれません。質問に対する回答がいつも「NO」である炎上目的の記事とは違って、ここでの回答は「YES」です。カスタムフックを作成可能です!
カスタムフックはルール3(名前が「use」で始まる必要がある)に従った単なる関数です。その後、それらの内部でフックを呼ぶこともできます。それはフックの束をまとめるための良い方法です。
たとえば、 AudioPlayer
コンポーネントの3つのstateを1つのカスタムフックに抽出することができます。
function AudioPlayer() {
// 3つのstateを抽出します
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);
// < 美しいオーディオプレイヤーがここに入ります >
}
つまり、stateを処理する新しい関数を作成し、再生の開始や停止を容易にするような追加メソッドを含むオブジェクトを返します。
function usePlayerState(lengthOfClip) {
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);
const stop = () => {
setPlaying(false);
setPosition(0);
}
const start = () => {
setPlaying(true);
}
return {
volume,
position,
isPlaying,
setVolume,
setPosition,
start,
stop
};
}
このようにstateを抽出することで、関連するロジックと振る舞いをグルーピングすることができます。stateと関連するイベントハンドラとその他の更新ロジックの「バンドル」を抽出し、コンポーネントのコードの見通しを良くするだけでなく、ロジックと振る舞いのバンドルを再利用可能にします。
さらに、カスタムフック内で別のカスタムフックを呼び出すことで、フックを統合することもできます。フックは単なる関数なので、当然、関数は他の関数を呼び出すことができます。
useState事例集
動作するuseStateフックの事例集をまとめました。異なる値を使い、オブジェクトを保存したり更新したりすることができます。(ヒント: this.setState
と全く同じではありませんが、近い動作になります)
ビデオレッスンもあるので、こちらもご覧ください。
黎明期
Hooks機能はわずか数日前にリリースされたため、誰もがビギナーです。それらはReactの課題解決のための新しいアプローチを提供し、エキサイティングな多くのアイデア(カスタムフック以上のものも)が今後数日、数週間、数ヶ月にわたり誕生するでしょう。
新しいものには興奮と不安がつきもので、多くの疑問もあるでしょう。
Reactチームは素晴らしいドキュメントのセットとFAQをまとめ、「全てのクラスコンポーネントを書き換えるべきか?」や「Hooksはrender内で関数を生成するので遅いのでは?」などの質問に答えているので、ぜひ目を通してみてください。
オープンなRFCがあり、ReactチームはHooksの実装に関するフィードバックを求めていますので、提案があればコメントを残してください。
そうそう、新しいCodeSandboxを開いて useState
フックで遊んでみてください!他の誰かが理解するまで待つ必要はありません。素晴らしい学びや、奇妙な問題が見つかったら、ぜひ共有してください!一緒にHooksを学んでいきましょう。
明日は useReducer
フックについての新たな記事をより詳細な事例とビデオと併せて投稿します。今週、他の全ての投稿について通知を受けるため、サインアップしてください。