皆さん、Reactしていますか?今回はReact 16.8から導入されたReact hooksのルールについて書きます。
React Hooksとは
従来のReactはいわゆるClass Componentで書いていくのが普通で、componentDidMountやconponentWillUnmountといったライフサイクルを用いて様々なアプリケーションを作っていく流れでした。
その後、Functional Componentが登場し、副作用のないシンプルなコンポーネントを関数として定義できるようになりました。
そして現在、Functional Component内にClass Componentにおけるライフサイクル相当のものが実装されました。
これがReact hooksです。
あくまで関数として上から下まで順に処理をしていくことで、副作用を抑える仕組みとなっています。
hooksには様々なAPIが用意されていますが、どれもFunctional Component内でuse*****という形式で呼び出します。
使う頻度の高いものをいくつか紹介します。
useState
変数と変数に値をセットするための関数が取得できるhookです。
従来のClass Componentにおけるstate, setStateと同様に扱うことができます。
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0); // [変数, 変数に値をセットする関数]
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect
ReactライフサイクルのcomponentDidMountとcomponentDidUpdateが合わさったもので、さらに関数を返すことでcomponentWillUnmountの処理も行うことが可能です。
つまり、毎回のRender後にuseEffectは呼ばれます。実際に例を見てみましょう。
const User = ({ username, getUser }) => {
useEffect(() => {
getUser(username);
}, [username, getUser]);
return (
{/* 略 */}
);
};
usernameを引数に取り、ユーザー情報を取得する関数を呼んでいます。
ここで、useEffectの第二引数に注目してください。第二引数には依存パラメータの配列を設定することができ、このパラメータに変更があった場合のみ、useEffect内の処理が実行されます。それによって無駄に処理が走るのを防ぐことができます。
getUserは関数のため、依存パラメータとして指定する必要はありませんが、関数も時には動的である可能性があります。そのため、関数に変化がないことが確実だとしても、常に関数も指定しておく癖をつけておくと良いと思います。(指定したところで変化がなければ再実行される頻度は変わらないので)
ちなみに、eslinstの react-hooks/exhaustive-deps を導入すると、依存パラメータの指定し忘れがなくなるのでオススメです。
また、useEffect内で関数をreturnすると、コンポーネントのunmount時に実行されます。イベントリスナーの解除などはこのタイミングで行うと良いでしょう。
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// コンポーネントの破棄時にsubscribeを解除
subscription.unsubscribe();
};
});
useMemo
ちょっと難しいですが、こちらもよく使います。いわゆる変数のメモ化に使えるものです。具体例を挙げて示してみます。
const Example = () => {
const x = {/* 複雑な計算 */};
// 略
};
この場合、Sampleが再描画される度に複雑な計算が実行されることになります。useMemoを使うことでその計算結果を保持しておくことができます。
const Example = () => {
const x = useMemo(() => /* 複雑な計算 */, []);
// 略
};
useMemoにもuseStateと同様に第二引数に依存パラメータの配列を受け付けることができます。
例えば複雑な計算にyという値を使っているのであれば、yが変更された際には再計算が行われなくてはなります。このような場合に依存パラメータにyを指定してあげると、再描画時に処理が再実行されます。
useCallback
useMemoの関数版です。
一番使う場面はイベントハンドラーでしょう。input要素に文字列を入力する例を考えてみます。
import React, { useState } from 'react';
import TextField from '/some-path':
function Example() {
const [text, setText] = useState(''); // [変数, 変数に値をセットする関数]
return (
<div>
<TextField onChange={(e) => setText(e.target.value)} />
{/* 略 */}
</div>
);
}
input要素をデザインや機能追加したラッパーコンポーネントを用意することは多いと思います。ここでいうTextFieldはそういうものだと考えてください。
この例は一見問題ないですが、onClickの箇所で再描画されるごとに関数が生成・実行されています。
そのためTextFieldコンポーネントには毎回親から渡されるpropsに変更があると検知し、再描画されてしまいます。
そこでuseCallbackを使用します。
import React, { useState, useCallback } from 'react';
import TextField from '/some-path':
function Example() {
const [text, setText] = useState(''); // [変数, 変数に値をセットする関数]
const onChange = useCallback((e) => setText(e.target.value), [setText]);
return (
<div>
<TextField onChange={onChange} />
{/* 略 */}
</div>
);
}
useCallbackによってonChangeは毎回同じ関数を参照するため、TextFieldの再描画は起こらなくなります。
その他
他にもたくさんのhooksがあるので、公式ページを見てみてください。
また、hooksを自作することも可能です。こちらに関してもまた別の機会で紹介したいと思います。
React Hooksのルール
React Hooksは便利ですが、とある制約があります。
冒頭で記載した通り、React HooksはFunctional Component内で扱うことのできる機能です。
その内部の実装では、各HooksのMount時にHooks内のメモリに値や参照を順番通りに保存しています。
順番通りとはどういうことでしょうか。
Mount時と再描画時には以下のような処理が動きます。(公式例より)
// ------------
// Mount時
// ------------
useState('Mary') // 1. 'Mary'という文字列でstateを初期化
useEffect(persistForm) // 2. persistFormという処理をEffectとして登録
useState('Poppins') // 3. 'Poppins'という文字列でstateを初期化
useEffect(updateTitle) // 4. updateTitleという処理をEffectとして登録
// -------------
// 再描画時
// -------------
useState('Mary') // 1. Hooksに保存された変数を取得
useEffect(persistForm) // 2. persistFormという処理をEffectに再登録
useState('Poppins') // 3. Hooksに保存された変数を取得
useEffect(updateTitle) // 4. updateTitleという処理をEffectに再登録
// ...
Hooksはこれらの順番を記憶しており、順番が崩れると値にズレが生じてしまいます。よって以下のような処理を行うことはできません。
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
これはnameが空の時とそうでない時でuseEffectの実行有無が変わり、各Hooksの処理順に変化がおこってしまうためです。
もしFunctional Component内で条件分岐やループを行いたい場合は、全Hooksの処理の後ろに記載すれば大丈夫です。
まとめ
今回はReact Hooksについて紹介しました。
Class Componentの際のReactとは一風変わった実装となっていることが分かってもらえたかと思います。
今後はReact Hooks中心となった実装が当たり前となっていくので、まだ試していない方は今のうちからトライしておきましょう。