今日はReact16.8(2019/2~)で追加された、React Hooksについて紹介します。簡単に言ってしまうと、ステートと副作用を関数コンポーネントで使うための機能です。Hook登場前は、関数コンポーネントではステートを扱えなかったので、ステートを扱いたい場合にはクラスコンポーネントを書いていました。
この記事では、「Reactは触ったことあるけど、まだHookを使ったことがない方」に向けて、React Hookの導入編と言う位置付けで書いていきます。初心者向けに書いていきますので、少々クドイ部分もあるかと思います。
Contents
React Hooksとは何か?
まずは、React Hooksの概要をざっくりと見ていきましょう。詳細は、後ほど解説します。
- React Hooksとは、ただの関数
- React16.8(2019/2~)で追加された
- 状態管理などのReactの機能を、クラスコンポーネントを書かずに使えるようになる機能。言い換えれると、関数コンポーネントに機能を追加することができる機能。
- コンポーネントの state は完全に独立している。フックは state を用いたロジックを再利用するものであって、state そのものを再利用するものではない。実際、フックのそれぞれの呼び出しが独立した state を持つので、全く同じフックを1 つのコンポーネント内で 2 回呼び出すことも可能。
と箇条書きにしましたが、この時点ではしっくりこないかもしれません。この後の、コード例を見た後に、再度読んでみてください。
クラスコンポーネントにおけるthis.stateとuseState Hookの違い
ここで、クラスコンポーネントにおけるthis.state
とuseState
Hookの違いを確認しておきます。
- useState Hookでは、新しいstate が古いものとマージされない。
- useStateのstateはオブジェクトである必要はない。
React Hooks登場前は、クラスコンポーネント内でthis.sateを用いてローカルな状態管理を行っていました。関数コンポーネントでは、状態管理をすることができなかったので、関数コンポーネントのことをステートレスコンポーネント(状態を持たないコンポーネント)とも呼んでいましたが、現在はHookによりstateを持つことがでようになったので、このような関数は、「関数コンポーネント (function component)」と呼ばれています。
React Hooks全般における使用のルール
個別のReact Hookの解説に入る前に、React Hook全般について言えるルールを、まず確認しておきます。
- フックは、Reactの関数のトップレベルのみで使える。ループや条件分岐やネストした関数の中でフックを呼び出さない。条件付きで、フックを実行したい場合には、その条件をフックの中に書くべし。
- フックは React の関数コンポーネントの内部のみで呼び出す。通常の JavaScript 関数内では呼び出さない。
このようなReact Hook全般についてのルールをプロジェクト内で浸透させるためには、eslintのプラグインである eslint-plugin-React-Hooksを利用するのが良いでしょう。ついつい、アンチパターンで記述してしまった際に、リンターが教えてくれるので便利です。
useState Hookの使い方
まずは、基本のHookからです。useState
Hookの使い方はとてもシンプルです。
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
//以下省略
const [<stateの変数名>, <stateを更新する関数名>] = useState(<stateの初期値>);
上記のコードは、JavaScript初心者には慣れない書き方かもしれませんが、これは、 JavaScript の構文の一つで分割代入(destructuring)と言います。
ReactのuseState関数からは、二つの値が返ってきます。一つ目が、state。二つ目がそのstateを更新する関数です。その二つに、名前をつけている(変数にアサインしている)と言うことです。
ここで、分割代入を簡単な例で説明しておきます。
const array = ["あか", "あお", "きいろ"];
// 分割代入
const [ red, blue, yellow] = array;
console.log(red); // => "あか"
console.log(blue); // => "あお"
console.log(yellow); // => "きいろ"
つまり、変数を宣言しているのと同じなので、変数名は自分の好きなようにつけてOKですが、インデックス通り(順番通り)にアサインされるので、useState Hookの場合、一つ目がstateで、二つ目がそのstateを更新する関数となることに注意してください。
state が「作成」されるのはコンポーネントの初回レンダー時だけです。次回以降のレンダー時には、useStateからは既存のstateの現在値を受け取ります。
useEffect Hookの使い方
いよいよ、React Hookの中で核となるuseEffectです。
Reactで言うとこの「副作用 (side-effect/effect)」とは
まずは、抑えておきたい言葉の定義からです。
Reactで言うとこの「副作用 (side-effect/effect)」とは、データの取得、購読 (subscription) の設定、あるいは React コンポーネント内でのDOMの手動での変更などのことを指します。
Reactでの副作用が何か、ここでしっかりと頭に入れておいてください。Reactの公式ドキュメントを読むと、「副作用」と言う言葉が何度も使われますので、副作用が何か見失うと、React Hookの理解に苦しむで、初心者の方は注意です。
useEffect Hookの基本
useEffect(() => {
document.title = `You liked ${count} times`;
}, []);
useEffect
Hook は、2つの引数をとります。
- 副作用の関数
- 依存配列(英語でdependenciesとかdepsと呼ばれます)
依存配列を指定する場合、 コンポーネント内の値がありエフェクトでも使われてる場合は、全て記述してください。全てとは、コンポーネント内の props, state, そして関数も含みます。React は依存配列の中身の値を比較し、エフェクトをスキップするかどうか判断しています。依存配列は、省略することができますが、そうすると、コンポーネントがレンダーされるごとに、第一引数の副作用関数が実行されます。本当に、レンダー毎に実行する必要があるか注意しましょう。こう言う些細な見落としが積み重なり、アプリケーションのパフォーマンスに悪影響を及ぼしていきます。後からパフォーマンスチューニングするとなると結構苦労します。
ちなみに、[]
(empty array)を指定すると、初期レンダー後に一度だけエフェクトが実行されます。この[]
の実装には注意が必要です。本当に、エフェクトはエフェクトの外から、関数、props、stateを、何も用いてないか再確認してください。
useEffectの特徴
useEffect
の特徴を箇条書きでまとめると、
- コンポーネントのレンダー後に行う処理(副作用)を
useEffect
内に記述する。「DOMの操作」「ウェブAPIとの通信」など。つまり、先ほど説明した副作用をuseEffect
内に記述する。DOMの操作に関していえば、そもそも要素がDOM上に存在しないと操作できないので、レンダー後に実行する必要があるので、useEffect
内に記述する。「ウェブAPIとの通信(非同期通信)」は、要素のレンダリングには関係ない処理なので、useEffect内に書くと言う取り決め。 - デフォルトでは、副作用関数は初回のレンダー時と、毎回の更新時に呼び出される。
componentDidMount()
とcomponentDidUpdate()
を合わせたようなイメージ。と言いながらも、Reactコア開発者のDan氏は、「effects とcomponentDidMount
や他のライフサイクルメソッドのメンタルモデルは別である。なので、それぞれのライフサイクルメソッドの代用を探そうとすると余計に混乱してしまう。ちゃんと理解するためには「エフェクトで考える」必要があり、そのメンタルモデルはライフサイクルイベントに反応することではなく props や state の変化を DOM にシンクロさせる、という方に近い」と語っておられます。
ここから、useEffect
のコード例を以下のパターンに分けて解説していきます。
- クリーンアップコードを必要としない副作用
- クリーンアップコードを必要とする副作用
クリーンアップコードを必要としない副作用
クリーンアップを必要としない副作用とは、手動での DOM の更新、ログの記録などです。
まずは、復習も兼ねて、Hook登場前には、Reactクラスコンポーネントでどのように実装していたのかを復習しておきましょう。
Reactクラスコンポーネントでの例
class LikeMe extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You liked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You liked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You liked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Like me
</button>
</div>
);
}
}
ここに出てくるライフサイクルメソッドについて、簡単に説明しておきます。
componentDidMount()
は コンポーネントがマウント(DOMにノードを追加)された後に実行されるメソッドです。用途の例は以下のとおりです。React Hookで言うところの副作用ですね。
- データフェッチを行う(初回のみ)
- DOM に対する処理を行う(初回のみ)
- タイマーをセットする
- イベントリスナのセット
ここで重要な点は、componentDidMount()
は一度のみ実行されると言う点です。props
や state
の変更に応じて再フェッチする場合などは componentDidUpdate()
にも処理を書く必要があります。
一方で、componentDidUpdate()
は、コンポーネントの props
または state
が変更されたときに実行されます。用途の例は以下のとおりです。
- データフェッチを行う(二回目以降)
- DOM に対する処理(二回目以降)
- その他諸々
と言うことで、初回のデータフェッチは、componentDidUpdate()
では対応することができません。
上記のコードサンプルの中で言うと、
document.title = `You liked ${this.state.count} times`;
のコードをcomponentDidMount()
とcomponentDidUpdate()
に、二度繰り返し記述する必要がありました。ここがポイントです。多くの場合、コンポーネントがマウント直後なのか更新後なのかに関係なく、同じ処理(副作用)を実行したいからです。この問題を解決するのがuseEffect
です。それでは、コード例をみていきます。
useEffect Hookでの例
先ほどのコードをuseEffect Hookを用いて書き直してみましょう。
import React, { useState, useEffect } from 'react';
function LikeMe() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You liked ${count} times`;
});
return (
<div>
<p>You liked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Like me
</button>
</div>
);
}
と、かなりシンプルに書けるようになりました。
クリーンアップコードを必要とする副作用
次に、クリーンアップを必要とする副作用についてみていきます。クリーンアップを必要とする副作用とは、何らかの外部のデータソースへの購読をセットアップし、その後、解除を行うような場合です。解除しないとメモリリーク(メモリの空き領域が減っていく現象)が問題となる場合があります。
useEffect Hookでの例
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const countId = setInterval(() => setCount(new Date().getTime()), 1000);
// クリーンアップする関数を登録する。ちなみに、clearInterval()は、setInterval()でセットしたタイマーを解除する関数。
// Returnするのは関数である必要あり。
return () => clearInterval(countId);
});
return <div>{count}</div>;
};
ReactDOM.render(<Counter />, document.getElementById("container"));
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
」と言う警告がブラウザのコンソールに表示されるので、この警告をみたら、クリーンアップし忘れていないか確認してみてください。カスタム(独自)Hooksの作り方
カスタムフックとは、自分で作ったフックのことです。関数ですので、何を引数として受け取り、何を返すかは、好きに指定することができます。
カスタムHooksはどんなケースで使えるのか?
state を用いたロジックをコンポーネント間で再利用したい時に、カスタムフックが役に立ちます。
これまでは、Higher order component(HOC)とrender propsというパターンが主な方法でした。
カスタムフックは、機能というよりはむしろ慣習のようなもので、関数の名前が ”use” から始まって、その関数が他のフックを呼び出しているなら、それをカスタムフックと言うことにする、という取り決めです。
この useSomething という命名規約によって、eslint-plugin-React-Hooksプラグインはフックを利用しているコードのバグを見つけることができます。カスタムフックもHookに変わりないので、上記で解説したReact Hookのルールが適用されるということです。ですので、関数名は、必ずuseSomethingというように、useから始まるようにしましょう。
カスタムフックのレシピ集(参考コード)のようなものはReactコミュニティによりすでにたくさん存在しています。慣れない間は、以下の記事などを参考にすると良いでしょう。英語圏では、それなりに評判のあるサイトです。ページ内のコードを読んでいけば、 Hookの実践的な使い方が身につくでしょう。
useLayoutEffectの使い方
useLayoutEffect
と useEffect
の決定的な違いは、hookが呼び出されるタイミングです。
useEffect
は、「コンポーネントのレンダリングが完全に完了した後」に実行されるのに対して、useLayoutEffect
は、「DOMに要素が追加され、ブラウザに表示される直前」です。
つまり、useLayoutEffect
を使うと、useLayoutEffect
の中に書いた処理によって画面の更新がブロックされます。
実行される順番をまとめると下記の通りです。
- propsやstateの変更などによりReactのレンダーが走る
useLayoutEffect
の中に書いた処理の実行useLayoutEffect
の中に書いた処理の終了- 画面の更新
といった、流れになります。
では、いつ使うのか?という疑問が残りますが、実際には、ほとんどのケースではuseEffect
で十分であるようです。
使えるケースですが、Reactでの開発経験がある方なら見たことあるかもしれませんが、stateやpropsが更新されて、画面が再描画される際に、一瞬フリック(チラつく)するようなときです。例えば、本当は、100を画面に表示したいのに、一瞬0が表示されて、100に更新されるようなときです。
React公式documentにも「まず useEffect
で始めてみて、それで問題が発生する場合にのみ useLayoutEffect
を試すことをお勧めします」と、書いてありますので、補助的なHookといった感じです。
useReducerの使い方
reducerとはそもそも何か?
Reduxを扱ったことがあれば、なんとなくイメージつくかと思いますが、まず「reducerとは、2つの値を受け取り、何かの処理をして、1つの値を返す関数」のことを言います。Array.prototype.reduce()のメソッドもreducerです。useReducer hookを解説する前に、JavaScriptのreduceメソッドをみておきます。
// totalの初期値を0に設定する
const numbers = [1, 2, 3];
const sum = numbers.reduce((total, number) => {
return total + number;
}, 0);
console.log(sum) // 6
// totalの初期値を10に設定する
const numbers = [1, 2, 3];
const sum = numbers.reduce((total, number) => {
return total + number;
}, 10);
console.log(sum) // 16
配列のメソッドですので、map()などと同様に配列の要素数だけ、コールバック関数が実行されます。この例では、3回です。
- total=0, number=1, sum=1
- total=1, number=2, sum=3
- total=3, number=3, sum=6
と処理が進み、最終的なsumは6となります。
Array.prototype.reduce()のメソッドの第二引数は、totalの初期値です。totalの初期値を10に設定すると、10+1、11+2、13+3=16となります。簡単ですね。
これを踏まえ、useReducerを解説していきます。
useReducerとは?
まずは、簡単なuseReducerの例をみていきます。
useReducerフックは、2つの引数をとります。第一引数にreducer関数、第二引数に初期値(initial state)です。そうです、先ほどのreduceメソッドにそっくりです。
useReducer(<reducer関数>, <初期値(initial state)>);
そして、useReducerからリターンされるのは、現在のstateとアクションを発火させるdispatch関数が格納された配列です。ここは、先ほど説明したuseStateフックにそっくりです。
これまでの説明をまとめると以下のようになります。
const [state, dispatch] = useReducer(reducer関数, 初期値);
先ほど説明したように、useReducer は2つの要素からなる配列を返すので、それを分割代入しているだけですね。
それでは、React公式docの例よりもさらにシンプルなuseReducerの使用例をみていきます。
useReducerのシンプルなコード例
「ボタンを押す毎に、+1されるだけの関数コンポーネントです。」
function AddOne() {
const [sum, dispatch] = useReducer((state, action) => {
console.log('state, action', state, action)
return state + action;
}, 100);
return (
<>
<div>{sum}</div>
<button onClick={() => dispatch(1)}>Add + 1</button>
</>
);
}
念のためstateとactionに実際何が格納されているのかコンソールログを見ておきましょう。
初期値を100に設定しているので、100からスタートです。クリックするごとにdispacth関数経由でactionに1を渡しています。ここので注目ポイントは、actionが1だと言うことです。dispatch(1)と1を渡しているので当然なんですが、reduxに慣れ親しんだ方なら、{ type: “ADD_ONE”, value: 1 }と書きたくなったかもしれません。もちろんそのようにオブジェクトで書いてもいいのですが、useReducerはreduxに似ているけどreduxではないことを強調するために、ここではあえて、バリューの数字のみを使用しました。(この例では、action.typeとtypeごとに処理をスイッチする必要もないので)
useStateではなくuseReducerを使うべきケースとは?
React公式docには、このように説明があります。
通常、
useReducer
がuseState
より好ましいのは、複数の値にまたがる複雑な state ロジックがある場合や、前の state に基づいて次の state を決める必要がある場合です。また、useReducer
を使えばコールバックの代わりにdispatch
を下位コンポーネントに渡せるようになるため、複数階層にまたがって更新を発生させるようなコンポーネントではパフォーマンスの最適化にもなります。
上記の説明を分解し、useReducerを使うべきケースを、それぞれ見ていきましょう。
- 複数の値にまたがる複雑な state ロジックがある場合
- 前の state に基づいて次の state を決める必要がある場合
- 複数階層にまたがって更新を発生させるようなコンポーネント(パフォーマンスの最適化)
1.複数の値にまたがる複雑な state ロジックがある場合
例えばサインアップフォームの実装などです。名前、Email address、パスワードなど、いくつかのstateを設定する必要があります。
const [name, setName] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
useStateを使うと、個別にステートを設定する必要があります。コードの可読性が落ちます。useReducer を使えば、ステートを一括で設定することが可能です。
2.前の state に基づいて次の state を決める必要がある場合
これはイメージしやすいですね。先ほどの、「ボタンを押す毎に、+1されるだけの関数コンポーネント」のようなケースのことです。
3.複数階層にまたがって更新を発生させるようなコンポーネント(パフォーマンスの最適化)
これに関しては、素晴らしい記事を見つけたのでリンクを掲載しておきます。ここまで読んでこれた方なら、難しくないはずです。じっくり、読んでみてください。
「useReducerの本質:良いパフォーマンスのためのロジックとコンポーネント設計 by @uhyo」
React hookのまとめ
このようにReact hookだけでもかなりの分量があるので、初心者の方は焦らずに一つづ理解していけば良いでしょう。今回の記事は、まだまだhookの導入レベルです。
既存のコードがクラスコンポーネントで記述されていて、それをまねてクラスコンポーネントで書くのではなく、今から新しくコンポーネントを書いていくなら、関数コンポーネントにしてどんどんhookを利用していけば、プロジェクトのレベルも、hookの理解も深まると思います。チャレンジあるのみです!