React - Hook
Introducing Hooks
Hook とは
Hook とは、React 16.8 でリリースされた、ステートやライフサイクルメソッドを持つコンポーネントをファンクションで作成するための仕組み。
また、React の state やライフサイク ルメソッドにhook into
(接続する)ための関数のこと。
いままで(クラス)
class Example extends React.Component {
constructor(props) {
this.state = { count: 0 };
}
render() {
return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => this.setState((state) => ({ count: state.count + 1 }))}
>
Click me
</button>
</div>
);
}
}
これから(Hook)
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
Hooks が生まれた背景
ステートフルなロジックを簡単に再利用するため
これまでは、ステートフルなロジック(State + Side Effect(Lifecycle Method))を React に注入するためにrender props
やHOC
を使ってきたが、それらは下記の問題を抱えていた。Hook はこれらの問題を全て解消する。
- コンポーネントを再構成しないとステートフルなロジックを再利用できない
- コードが読みにくくなる(Wrapper Hell)
- コンポーネントからステートフルなロジックを分離できない
- ステートフルなロジックのテストと、コンポーネントのテストを分離できない
コンポーネントをシンプルにするため
今までは、componentDidMount
といった一つのライフサイクルメソッドに、複数の無関係な処理(デー タ取得、何らかの初期化処理、その他)がごちゃまぜになっていた。これにより、コードが読みにくくなり、バグの温床にもなっていた。
Hook を使うことにより、これらの処理を「ライフサイクルメソッドごと」ではなく、「関連する機能ごと」で関数に分割できる。
JavaScript のクラスは人にもマシンにも良くない
this
やbind
など、JavaScript のクラスは変な癖があり、多くの初学者にとってハードルが高い。
また、マシンにとっても、クラスが使ってあることで minify や最適化をやりにくくなる。
段階的な移行のススメ
クラスベースのコンポーネントを廃止する計画はない。既存のコードを Hooks で書き直すことはせずに、新規コードから徐々に移行することをおすすめする。
Hooks の概要
State Hook
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
useState
が State Hook である。useState
は、function component が複数回呼ばれた(再レンダリングされた)場合も、前回の state の値を保持しているuseState
は、値と、値を更新するファンクションを返す。useState
には初期値を渡す。- 2 回目以降の render 時には、
useState
に渡した値(初期値)は単に無視される。 this.setState
と異なり、値はオブジェクトでなくてもよい。this.setState
と異なり、オブジェクトをマージするような機能はない。
なぜcreateState
という名前ではないのか
初回の render 時に state を「作成」するだけでなく、2 回目以降の render 時には state を「取得」する役割があるから。
複数の State を使う
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}
Effect Hook
- Side Effects(≒effects)を Hook で記述する際は、Effect Hook を使う。
- Side Effects とは、データの取得やサブスクリプションなどの処理のこと。一般的にはライフサイクルメソッドとして書かれる。
- Side Effects の名前の由来
- render 内に書くと頻繁に呼ばれすぎるため、render の外(=side)に記述すること
- コンポーネントの描写に影響を与える(=effect)こと
import React, { useEffect } from 'react';
function Example() {
useEffect(
() => {
// この領域が、`componentDidMount`と`componentDidUpdate`に該当
document.title = `You clicked ${count} times`;
return () => {
// この領域が、`componentWillUnmount`と`componentDidUpdate`に該当
document.title = '';
};
},
// ここに`componentDidUpdate`でwatchしたいstate or propsを指定する。
// 省略した場合は、renderのたびに毎回呼ばれる。
// 空配列を指定した場合は、マウント時に1回だけ呼ばれる。
[count],
);
return <div />;
}
useEffect
が Effect Hook である。- 下記の 3 つを 1 つにまとめたものである
componentDidMount
componentDidUpdate
componentWillUnmount
- デフォルトではレンダリング後に毎回呼ばれる。
componentDidMount
とcomponentDidUpdate
が一緒になっているようなもの。- props が変化するたびに毎回呼ばれることで、props の変化を漏らさず捕捉し、必ず適切なクリーンアップを毎回行い、もってバグを減らすことができる
useEffect
に記載した内容は、レンダリングの後に行われる(でなければ useEffect を使う意味が薄れる)componentDidMount
などと異なり、useEffect
はレンダリングをブロックしない。ブロックしたい場合は、類似品のuseLayoutEffect
を使うこと。
useEffect が呼ばれる条件を変更する
「props が変化するたびに毎回呼ばれる」という動作を変更するには、第 2 引数に、変化を補足したい state や props を指定する。空配列([]
)を指定すると、初回マウント時にだけ実行される(componentDidMount
と同じ動作になる)。
いずれ、これらの配列を明示的に渡さなくても、自動で最適化されるようになる予定らしい。
useEffect(() => {}, [count]); // 初回及び`count`の値が変化した時に実行される
useEffect(() => {}, []); // 初回のみ実行される
複数の Effect Hook
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => { /* do something */});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => { /* do something */});
})
Hook に関する絶対的ルール
- Hook は 「React function component」及び「カスタム Hook」 のトップレベルでのみで使え
- Hook をループや if 文や子ファンクションの中で呼ぶな
- Hook は 通常の関数から呼ぶな
eslint-plugin-react-hooks
を使ってルールが守られているかチェックしろ
カスタム Hook を作る
カスタム Hook を作ることで、ロジックを再利用可能にすることができる。
カスタム Hook は、React の機能というよりは、関数を使って Hook に関する重複処理を排除するための、ただの慣習である。
例えば、ユーザのログイン状態を表す state をコンポーネントに注入する Hook の例は以下の通り(いままでなら HOC などを使ってやらざるを得なかったところ)
カスタム Hook
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
コンポーネント
// コンポーネント1
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
// コンポーネント2
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>{props.friend.name}</li>
);
}
- Hook は、「State 自体」ではなく、「State を提供するロジック」を保持している。このため、上記の 2 つのコンポーネントにある
isOnline
は完全に独立している。 - カスタム Hook は、アニメーション、サブスクリプション、タイマー、ブラウザイベントなど、どんなものでも対象にできる。
- カスタム Hook の名前は
use****
の形式でつけること。
その他の Hook API
useContext
--- コンテキストを使うuseReducer
--- リデ ューサーを使うuseCallback
--- コールバックをメモ化して子コンポーネントに渡す時に使う。無駄な再描写を防ぐために使う。useMemo
--- メモ化の機能を使いながら値を計算する。パフォーマンス最適化のために使う。useRef
--- DOM への参照を取得するときに使うuseImperativeHandle
--- 親から子を ref で操作する時に使うっぽいがよくわからないuseLayoutEffect
--- 同期版のuseEffect
である。従来のcomponentDidMount
等と同じ。