メインコンテンツまでスキップ

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 propsHOCを使ってきたが、それらは下記の問題を抱えていた。Hook はこれらの問題を全て解消する。

  • コンポーネントを再構成しないとステートフルなロジックを再利用できない
  • コードが読みにくくなる(Wrapper Hell)
  • コンポーネントからステートフルなロジックを分離できない
  • ステートフルなロジックのテストと、コンポーネントのテストを分離できない

コンポーネントをシンプルにするため

今までは、componentDidMountといった一つのライフサイクルメソッドに、複数の無関係な処理(データ取得、何らかの初期化処理、その他)がごちゃまぜになっていた。これにより、コードが読みにくくなり、バグの温床にもなっていた。

Hook を使うことにより、これらの処理を「ライフサイクルメソッドごと」ではなく、「関連する機能ごと」で関数に分割できる。

JavaScript のクラスは人にもマシンにも良くない

thisbindなど、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
  • デフォルトではレンダリング後に毎回呼ばれる
  • 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等と同じ。