来たる 2 月 4 日、ついに React 16.8 の正式版がリリースされます。この React 16.8 には、アルファ版が公開されて以来常に React ユーザーたちの関心をほしいままにしてきたReact Hooksが正式版となって追加されます。
※追記:アメリカ時間で 2 月 5 日になっても React 16.8 がリリースされませんでした。事前情報で 2 月 4 日と言ってたのに……。いつになったらリリースされるのかはよく分かりません。2 月 6 日に React 16.8 がリリースされました!
熱心な React ユーザーの方は、当然 React Hooks の情報を常に追っており正式版がリリースされたらすぐにでも自分のコードで使いはじめる準備ができていることと思います。しかし、この記事をご覧の方々の中には React を使っているにも関わらず「React Hooks のことはよく分からない」とか「聞いたことくらいはあるけど今どうなっているのか知らない」とか「正式版が出てから調べればいいやと思っていた」という方がもしかしたら居るかもしれません。
いよいよ正式版の登場と相成ってしまいましたから、皆さんはもう React Hooks から逃げることはできません1。この記事では上記のような方々を対象として、React Hooks の正式版に備えてどんなフックがあるのかをすべて説明します。アルファ版のうちは API の変化もありましたから、少し前にちょっと調べてみたけど今どうなっているのかは分からないというような人は要チェックです。なお、React はドキュメントの充実に力を入れており、React Hooks に関しても公式ドキュメントがちゃんと最新の状態に保たれています。ですから、英語が読める人はこの記事を読まずに公式ドキュメントを読むのもよいかもしれません。
記事中に登場するサンプルコードはGitHubに置いてあります。
React Hooks の概要
総ざらいというタイトルをつけた記事なので、一応 React Hooks について基礎から説明しておきます。React Hooks は、一言で言えば関数コンポーネントから使える新しい APIです。これまで、関数コンポーネントは引数として props を受け取り、レンダリング結果を返り値で返すというインターフェースで定義され、ステートなどの複雑な機能を持たない単純なコンポーネントを作れるものとして提供されていました。初期には Stateless Function Component(状態なし関数コンポーネント)という用語が使われていたことからも分かる通り、もともと関数コンポーネントは、クラスコンポーネントが持つようなstate
の機能やcomponentDidMount
を始めとするライフサイクルメソッドを持ちませんでした。
React Hooks によって、関数コンポーネントにこれらの機能を持たせることができるようになります。すなわち、従来はクラスコンポーネントでしか出来なかったことが関数コンポーネントでもできるようになるのです。ただ一つ注意していただきたいのは、React Hooks はクラスコンポーネントの API をただ移植したわけではないということです。React Hooks の API は新しくデザインし直された API であり、従来の API の問題点が解消されています。例えば、フックの利用方法をただの関数呼び出しとすることによって、フックの合成(=カスタムフックの作成)を容易にしています(カスタムフックについては筆者の以前の記事が参考になるかもしれません)。
React Hooks は、API 上はuse
から始まる名前のただの関数です。react
パッケージからuseState
やuseEffect
などの関数がエクスポートされています。裏では React が今どのコンポーネントを処理しているかといった情報を保持しており、これらの関数はその情報に基づいて適切に動作します。現在のところ、React Hooks の API は関数コンポーネント専用であり、それ以外のシチュエーションで呼び出すことはできません。
基本のフック
先述のドキュメントでは、useState
, useEffect
, useContext
の 3 つが基本的なフックとして挙げられています。それに倣ってここでもまずはこの 3 つを紹介します。
useState
useState
フックは、関数コンポーネントにステートを持たせられる API です。このフックを使うと、クラスコンポーネントにおけるthis.state
に値を保存しておくのと同じような感じで、コンポーネントに状態を持たせることができます。使い方はconst [stateの値, state更新関数] = useState(state初期値);
というのが基本です。
useState のサンプル
さっそくですが、useState
の使用例を見てみましょう。よくある例ですが、ボタンを押すと数値が増えたり減ったりするやつです。
import * as React from 'react';
const { useState } = React;
export const UseStateSample = () => {
const [count, setCount] = useState(0);
return (
<p>
<button onClick={() => setCount(count - 1)}>-</button>
<b>{count}</b>
<button onClick={() => setCount(count + 1)}>+</button>
</p>
);
};
このuseStateSample
は関数コンポーネントで、特に props を受け取らずに render 内容を返すという従来どおりのインターフェースを持っています。ポイントは、その処理の中でuseState
関数を呼び出している点です。
今回はcount
というステートを初期値0
で用意しています。よって、<b>{count}</b>
のところは最初は 0 が表示されます。2 つのボタンをクリックすると、setCount
が呼び出されるようになっています。setCount
を呼び出した場合は渡した引数でcount
ステートが更新されます。つまり、UseStateSample
の再render
処理が発生し、その際useState
によって返されるcount
の値が新しい値となっています。
クラスコンポーネントとの比較
上のサンプルと同じ処理を敢えて旧来のクラスコンポーネントで書くとこんな感じです。
export class UseStateSample extends React.Component {
state = { count: 0 };
render() {
const { count } = this.state;
return (
<p>
<button onClick={() => this.setState({ count: count - 1 })}>-</button>
<b>{count}</b>
<button onClick={() => this.setState({ count: count + 1 })}>+</button>
</p>
);
}
}
関数コンポーネントと Hooks で書いた場合に比べて煩雑ですね。その要因としては、クラスコンポーネントではthis.state
がオブジェクトであることや、this.setState
がそのオブジェクトのうち一部をアップデートするような API 設計になっていることが挙げられます。React Hooks の API ではcount
というステートに対してsetCount
という専用の関数がセットで用意されますから、{count: count+1}
のような余計なオブジェクト生成がなく直感的かつすっきりと書くことができています。
複数のステートを使う
関数コンポーネントでは、useState
を複数回呼ぶことで複数のステートを利用することができます。その場合、それぞれのステートに対して別々の更新関数が得られます。
export const UseStateSample2 = () => {
const [left, setLeft] = useState(0);
const [right, setRight] = useState(0);
return (
<p>
<b>{left}</b>
<button
onClick={() => {
setLeft(left + 1);
setRight(right - 1);
}}
>
←
</button>
<b>{right}</b>
<button onClick={() => setRight(right + 1)}>+</button>
</p>
);
};
この例ではコンポーネントはleft
とright
という 2 つのステートを持っています。それぞれのステートをアップデートするには対応する関数を用います。真ん中あたりに見えるように、複数のステートを同時に更新するには関数を全部呼びます。
これを見ると、ステートが複雑になってきたときにステートの更新が一発でできずにたくさんの関数呼び出しが必要になってしまうという懸念があるかもしれません。その場合はステートの値を数値などではなくオブジェクトにして一発で更新するほうがよいことがあります。このやり方を支援するフックとしてuseReducer
がありますので、あとで紹介します。
関数によるステートの更新
ステートの更新関数(上の例でのsetCount
など)を呼ぶ場合、今までのサンプルでは新しいステートの値を渡していました。下記の例では、このボタンを押すとcount
ステートを 1 増やす、つまり新しい値としてcount + 1
をセットしていることが分かります。
<button onClick={() => setCount(count + 1)}>+</button>
実は、更新関数には関数を渡すことができます。その場合、これは現在のステートの値を受け取って新しいステートの値を返す関数として解釈されます。つまり、上の例はこのように書き換えることができます。
<button onClick={() => setCount(count => count + 1)}>+</button>
これはsetCount
に関数を渡すことで、「現在のステートを読んで、それに 1 を足した値を新しいステートの値とする」ということを指示しています。このように、次のステートの値が現在のステートに依存する場合(すなわち現在のステートから新しいステートを計算する場合)は関数を用いた更新が適しています。
その理由の 1 つは、この方法だとステートの更新ロジックを純粋関数に抜き出すことができることです。さらにもう 1 つの理由として、関数を使わないとコールバックが複数回呼ばれる場合に正しく対処できないことがあります。
例として、「1 回押すと onClick が 5 回発生するボタン」というコンポーネントSuperButton
を作ってみました。このボタンを使うと 2 つの方法の違いが分かります。
const SuperButton = ({ onClick, children }) => {
const onclickHere =
onClick &&
(e => {
for (const _ of [0, 1, 2, 3, 4]) onClick(e);
});
return <button onClick={onclickHere}>{children}</button>;
};
export const UseStateSample4 = () => {
const [count, setCount] = useState(0);
return (
<p>
<SuperButton onClick={() => setCount(count - 1)}>-</SuperButton>
<b>{count}</b>
<SuperButton onClick={() => setCount(count => count + 1)}>+</SuperButton>
</p>
);
};
この例では、SuperButton
の onClick 関数で 2 種類の方法で書いたステートの更新を発生させています。既にお察しかと思いますが、() => setCount(count - 1)
のほうは、これが 5 回連続で呼び出されてもcount
は 1 だけ減ります。なぜなら、setCount(count - 1)
で参照されるcount
は常にこのUseStateSample4
コンポーネントがレンダリングされたときのcount
だからです。
それに対し、()=> setCount(count => count + 1)
の場合は、これが 5 回呼び出されることでcount
は 5 増えます。これは、この関数が呼び出されるたびに現在のcount
の値が参照される(そしてそれに 1 を足した数値が新しいcount
となる)からです。
このように、関数がワンパスで複数回呼び出されることを想定すると、関数を用いたステート更新(上の例で言えば後者)にしないと想定した動作にならないことがあります。繰り返しますが、現在のステートに依存して更新を行うならこのように関数をステート更新関数に渡しましょう。
useEffect
useState
と並んでよく使うであろうフックがこのuseEffect
フックです。これはレンダリング後に行う処理を指定できるフックです。クラスコンポーネントのライフサイクルでいえば、componentDidMount
及びcomponentDidUpdate
におおよそ相当するものです(実際には多少違うのですが、それは後で説明します)。
例えば、以下の例は 1 秒ごとに表示されている値が 1 増えるコンポーネントです。現在表示している値をステートで管理するために、さっき紹介したuseState
と組み合わせて実装しています。
import * as React from 'react';
const { useState, useEffect } = React;
export const UseEffectSample1 = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timerId = setTimeout(() => {
setCount(count => count + 1);
}, 1000);
// クリーンアップ関数を返す
return () => clearTimeout(timerId);
}, [count]);
return (
<p>
time: <b>{count}</b>
</p>
);
};
このように、useEffect
は 2 つの引数を取ります。返り値はありません。また、2 つ目の引数は省略可能です。1 つ目の引数はコールバック関数であり、この関数はコンポーネントのレンダリング完了時に呼ばれます。最初のレンダリングはもちろん、再レンダリングが発生したあとにも呼ばれます。ただし、第 2 引数でコールバック関数を呼ぶタイミングを制御できます。第 2 引数は値の配列であり、配列のいずれかの値が前回と変わったときのみコールバック関数を呼ぶという意味になります。省略した場合は無条件、つまりレンダリングの度に関数が呼ばれます2。
上のサンプルでは、まず最初のレンダリングが完了したタイミングでuseEffect
に指定したコールバック関数が呼びだされます。それにより、1 秒後にcount
ステートが更新されます。それによりコンポーネントの再レンダリングが発生し、再びuseEffect
のコールバック関数が呼ばれます。この繰り返しにより、このコンポーネントは 1 秒ごとに数値が 1 ずつ増えていくコンポーネントとなります。
今回第 2 引数は[count]
です。つまり、レンダリング終了時に、count
の値が前回のレンダリングと変わっていたらuseEffect
のコールバックを発火するということになります。今回の場合はステートがこれだけなので省略しても同じですが、他にもステートがある場合に余計な処理が発生しないようにする効果があります。
ここで呼ばれているコールバック関数は、よく見ると関数を戻り値としています。これはクリーンアップ関数です。クリーンアップ関数を宣言した場合は、次回のコールバックが呼ばれる前にクリーンアップ関数が呼ばれます。また、コンポーネントがアンマウントされる前にもクリーンアップ関数が呼ばれます。要するに、描画時にコールバック関数が呼ばれた場合、その描画が消されるときに対応するクリーンアップ関数が呼ばれるということです。とても便利ですね。
上の例では、正確には初回レンダリング →useEffect
のコールバック関数が呼ばれる →1 秒後にステートが変更されて再レンダリングが発生 →クリーンアップ関数が呼ばれる→useEffect
のコールバック関数が再び呼ばれる という流れをとっていることになります。ここではクリーンアップ関数でclearTimeout
を呼んでいますが、このようにコールバック関数で発生した副作用の後始末をするのがクリーンアップ関数の主な役目です。このクリーンアップ関数はsetTimeout
の発火後に呼ばれた場合は意味がありませんが、コンポーネントがアンマウントされてまだsetTimeout
が発火していない場合に、すでにアンマウントされたコンポーネントのステートを変更してしまうのを防ぐ意味があります。
useContext
useContext
は、指定したコンテキストの現在の値を得るフックです。コンテキストというのは React 16.3 で搭載された新しいコンテキスト API によるものを指しています。React.createContext
で作成したコンテキストオブジェクトをuseContext
に渡すことで、そのコンテキストの現在の値を返り値で得ることができます。
import * as React from 'react';
const { useState, useContext, createContext } = React;
const MyContext = createContext(() => {});
export const UseContextSample = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>
<b>{count}</b>
</p>
<MyContext.Provider value={() => setCount(count => count + 1)}>
<IncrementButton />
</MyContext.Provider>
</div>
);
};
const IncrementButton = () => {
const incrementHandler = useContext(MyContext);
return (
<p>
<button onClick={incrementHandler}>+</button>
</p>
);
};
この例は相変わらずボタンを押すと数値が増えるという単純なサンプルですが、数値を増やすボタンをIncrementButton
という別のコンポーネントに移しました。そうなると、ボタンを押したときの処理(UseContextSample
のステートを変更する)をIncrementButton
コンポーネントに伝える必要があります。一番単純な方法は props のひとつとしてその関数を渡すことですが、何段階も伝播させないといけないような場合には props を使うのは適していないことがあります。その場合はこの例のようにコンテキストを使って暗黙に値を伝播させます。
IncrementButton
を従来の API で書き直すとこんな感じです。コンポーネントのネストが無くなって綺麗に書けるのが嬉しいですね。
const IncrementButton = () => {
return (
<MyContext.Consumer>
{incrementHandler => (
<p>
<button onClick={incrementHandler}>+</button>
</p>
)}
</MyContext.Consumer>
);
};
そのほかのフック
ここまでの 3 つがとても良く使いそうなフックでした。それ以外のフックも続けて紹介していきます。比較的よく使いそうなフックから解説していきます。
useReducer
useReducer
は useState
の亜種であり、ステートを宣言するフックです。useReducer
は、ステートの初期状態に加えてreducer と呼ばれる関数を渡します。結果として、現在のステートに加えて、ステートを更新する関数の代わりに dispatch 関数が得られます。この reducer や dispatch というのは Redux 用語としてよく知られていますので、Redux を触ったことがあるかたは馴染み深い概念でしょう。
useReducer
によって作られたステートを更新する場合は、dispatch
関数にアクションを渡します。アクションというのはステートの更新指示を表す値で、別になんでも構いません。そのアクションを引数として、useReducer
に渡した reducer が呼び出されます。reducer というのは関数であり、アクションと現在のステートを受け取って新しいステートを返します。
import * as React from 'react';
const { useContext, useReducer, createContext } = React;
const DispatchContext = createContext(() => {});
const reducer = ({ year, month }, action) => {
switch (action) {
case 'increment':
return month === 11
? { year: year + 1, month: 0 }
: { year, month: month + 1 };
case 'decrement':
return month === 0
? { year: year - 1, month: 11 }
: { year, month: month - 1 };
}
};
export const UseReducerSample = () => {
const [state, dispatch] = useReducer(reducer, {
year: 0,
month: 1,
});
return (
<div>
<p>
<b>
{state.year}年{state.month}ヶ月
</b>
</p>
<DispatchContext.Provider value={dispatch}>
<ControlButtons />
</DispatchContext.Provider>
</div>
);
};
const ControlButtons = () => {
const dispatch = useContext(DispatchContext);
return (
<p>
<button onClick={() => dispatch('decrement')}>-</button>
<button onClick={() => dispatch('increment')}>+</button>
</p>
);
};
この例はこのようにレンダリングされます。
この例のように、useReducerには 2 つの引数を渡します。1 つ目の引数は reducer で、2 つ目の引数は初期状態です。今回useReducer
で宣言するステートはyear
とmonth
という 2 つのプロパティを持ったオブジェクトです。+
ボタンと-
ボタンを押すと、月が 1 つ増えます。12 ヶ月になると年が 1 つ増えて月が 0 に戻ります。(あくまでサンプルなので、年とか 12 で割って計算しろよという批判は受け付けません。)
ボタンが押されたときのロジックがreducer
関数にまとまっていることが分かりますね。reducer
の第 1 引数が今の状態で、第 2 引数がアクション(単純な例なので今回は文字列)です。そして、useReducer
により宣言されたステートの更新は、dispatch
関数にアクションを渡すことで行います。dispatch
を子コンポーネントであるControlButtons
に渡すのは先ほど紹介したuseContext
を使いました。
今回のポイントは、+
ボタンを押すとyear
とmonth
が両方同時に変化する可能性があるということです。このような複雑な変化をアクションという抽象的な命令で表すことによって、ステートを変化させる側(dispatch
を呼び出す側)の単純化とステート変更ロジックの分離を同時に達成しています。useReducer
を用いて色々な状態をひとまとめに宣言する場合、色々な子コンポーネントがdispatch
を用いて状態を変化させるはずですから、この例のようにuseContext
と組み合わせてdispatch
関数を子コンポーネントたちに伝えるのが特に適しています。ステートの更新はすべてdispatch
を通じて行うため、dispatch
関数ひとつ伝えれば十分であるというのも嬉しい点です。
関数によるステートの初期化
useReducer
の第 2 引数に初期ステートを渡す代わりに、関数を用いてステートを初期化することができます。この場合、useReducer
の第 3 引数に初期化関数を渡します。また、第 2 引数の意味が変わります。渡した初期化関数の引数としてuseReducer
第 2 引数が渡されます。この機能は、ステートの初期化ロジックを別の関数として定義したい場合などに便利です。
const reducer = ({ year, month }, action) => {
switch (action) {
case 'increment':
return month === 11
? { year: year + 1, month: 0 }
: { year, month: month + 1 };
case 'decrement':
return month === 0
? { year: year - 1, month: 11 }
: { year, month: month - 1 };
}
};
const init = initialMonth => ({
year: 0,
month: initialMonth,
});
export const UseReducerSample2 = ({ initialMonth }) => {
const [state, dispatch] = useReducer(reducer, initialMonth, init);
return (
<div>
<p>
<b>
{state.year}年{state.month}ヶ月
</b>
</p>
<DispatchContext.Provider value={dispatch}>
<ControlButtons />
</DispatchContext.Provider>
</div>
);
};
この例では、ステートの初期化関数init
を定義してuseReducer
の第 3 引数に渡しました。このコンポーネントを<UseReducerSample2 initialMonth={10} />
のように使うと、「0 年 10 ヶ月」の表示からスタートします。このように、第 2 引数を初期ステートではなくステートの元となる値にして、第 3 引数に渡したinit
関数でそれをステートに変換するという方式をとっています。
この場合は第 2 引数を{year: 0, month: initialMonth}
とするという手もありますが、初期ステートがコンポーネントの中で定義されているのはどうも微妙に思えます。そういう時に第 3 引数を使いましょう。
useReducerについてさらに詳しく理解する。
useReducer
がもたらす恩恵についてさらに詳しく書いた記事を用意しました。React Hooksに少し慣れたころに読むのが丁度いいかもしれません。
useMemo
useMemo
は、その名の通り値のメモ化に使えるフックです。第 1 引数に値を計算する関数を、第 2 引数に計算される値が依存する値の一覧を渡します。
import * as React from 'react';
const { useMemo } = React;
export const UseMemoSample = ({ n }) => {
const sum = useMemo(() => {
let result = 0;
for (let i = 1; i <= n; i++) {
result += i;
}
return result;
}, [n]);
return (
<div>
<p>
1 + … + n = <b>{sum}</b>
</p>
</div>
);
};
ここで定義したUseMemoSample
は、1 から指定した値までの和を表示するという意味不明なコンポーネントです。しかも、その和は for ループを回して計算します。<UseMemoSample n={1000} />
のように使うと1 + … + 1000 = 500500
と表示します。
この例はともかく、ループとか回す処理を render の中にベタ書きすると、レンダリングが行われるたびにそれが計算されることになります。計算結果をメモ化したい、すなわち以前計算した結果が再利用できるときは再利用したい、という場合にuseMemo
が役に立ちます。
useMemo
の第 1 引数は、値を計算する関数を渡します。それ自身に引数はありませんが、props の値などを使用しても構いません。その関数の返り値がuseMemo
の返り値となります。この関数が呼び出されるのは値の計算が必要となったときです。つまり、初回のレンダリング時および再計算が必要となったときに関数が呼び出されます。useMemo
は以前の計算の結果を覚えており、再計算が必要ない場合は渡した関数は呼び出されず、以前の計算の結果が返されます。
再計算がいつ必要かはuseMemo
の第 2 引数で指定します。これはuseEffect
の第 2 引数と同じで、ここに渡した値のいずれかが変化したときに再計算が行なわれます。今回の場合は計算結果はn
に依存しているため、第 2 引数に[n]
を渡す必要があります。これにより、n
が変化したとき再計算が行なわれるようになります。
useCallback
useCallback
はuseMemo
の亜種です。簡潔に言えば、useCallback(fn, arr)
はuseMemo(()=> fn, arr)
と同じです。つまり、useCallback
は計算の必要ない値をメモ化するときに便利なフックです。
計算が必要ないのにメモ化とはどういうことかとお思いかもしれませんが、useCallback
という名前が示唆するとおり、これは関数をメモ化するのに便利です。ポイントは、()=> { ... }
のような関数式は毎回新しい関数オブジェクトを作るという点です。
以下にuseContext
のサンプルを再掲します。
export const UseContextSample = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>
<b>{count}</b>
</p>
<MyContext.Provider value={() => setCount(count => count + 1)}>
<IncrementButton />
</MyContext.Provider>
</div>
);
};
ここでMyContext.Provider
の prop として()=> setCount(count => count+1)
という関数を渡しています。これは関数式なので、UseContextSample
がレンダリングされるたびに新しい関数オブジェクトが作られてそれがMyContext.Provider
に渡されます。実はこれは良くありません。なぜなら、コンテキストに渡された値が変化するたびにそのコンテキストの値を使用するコンポーネントは全部再描画されるため、上の例でMyContext
を使用するコンポーネントが毎回再描画されてしまうからです。
こういう時はuseCallback
の出番です。以下のようにすることで再描画を防ぐことができます。
import * as React from 'react';
const { useState, useContext, useCallback, createContext } = React;
const MyContext = createContext(() => {});
export const UseCallbackSample = () => {
const [count, setCount] = useState(0);
const updateCount = useCallback(() => setCount(count => count + 1), []);
return (
<div>
<p>
<b>{count}</b>
</p>
<MyContext.Provider value={updateCount}>
<IncrementButton />
</MyContext.Provider>
</div>
);
};
const IncrementButton = React.memo(() => {
const incrementHandler = useContext(MyContext);
return (
<p>
<button onClick={incrementHandler}>+</button>
</p>
);
});
この例では、MyContext.Provider
に渡す関数をuseCallback
を用いてメモ化しています。第 2 引数が[]
ということは、一度初期化された後はupdateCount
は毎回同一の関数オブジェクトとなります。よって、MyContext.Provider
の値は変化していない扱いとなり、IncrementButton
は再描画されなくなります。
ただし、IncrementButton
がReact.memo
で囲うように変更されている点に注意してください。これはフックではないので詳細は省きますが、親のUseCallbackSample
が再描画されても自動的にIncrementButton
が再描画しないようにする効果があります。この 2 つの施策によりIncrementButton
が再描画される要因が無くなります。このようにuseCallback
(やuseMemo
)は最適化に利用することができます。
useRef
useRef
は、ref オブジェクトを作って返すフックです。この ref オブジェクトはReact.createRef
を使って作ることができるオブジェクトのことです。useRef
は、同じ呼び出しに対しては同じ ref オブジェクトを返します。ref オブジェクトはcurrent
プロパティにその中身が入っています。current
の初期値はuseRef
の引数で指定することができます。
useRef
のひとつの用途は、コンポーネントのref
属性に渡すためのrefオブジェクトを作ることです。useEffect
と組み合わせた例を作ってみました。
import * as React from 'react';
const { useEffect, useRef } = React;
export const UseRefSample = () => {
const displayAreaRef = useRef();
useEffect(() => {
let rafid = null;
const loop = () => {
// 現在時刻を表示
const now = new Date();
displayAreaRef.current.textContent = `${String(now.getHours()).padStart(
2,
'0',
)}:${String(now.getMinutes()).padStart(2, '0')}:${String(
now.getSeconds(),
).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}`;
rafid = requestAnimationFrame(loop);
};
loop();
return () => cancelAnimationFrame(rafid);
});
return <p ref={displayAreaRef} />;
};
このコンポーネントはこのような表示となります。
これは、requestAnimationFrame
を用いてリアルタイムで現在時刻をミリ秒単位で表示するコンポーネントです。このような高頻度な更新をステートを用いて行うのは負荷が高そうな気がするのでuseEffect
を用いて直接 DOM 操作を行っています。
直接 DOM 操作を行うには、レンダリング後に生の DOM ノードを取得する方法があります。その方法がコンポーネントのref
属性(<p ref={displayAreaRef} />
)です。このref
属性に渡すための ref オブジェクトをuseRef()
フックで作っています。ref
属性の効果により、レンダリング時に ref オブジェクトのcurrent
プロパティに DOM ノードがセットされます。この例ではuseEffect
のハンドラの中からこの情報を使用しています。
レンダリング間変数として useRef を使用する
従来、ref オブジェクトというのは専ら、上記の例のように DOM オブジェクトへの参照を得るのに使われてきました。しかし、フックの世界においてはuseRef
や ref オブジェクトはその便利さをさらに増しています。
ポイントは、useRef
が返すオブジェクトはレンダリング間で毎回同じであるということです。特にuseEffect
を使用する場合に、レンダリング間で情報を共有する場合に使用可能です。
例えば、componentDidUpdate
は「前回の props」と「前回の state」を参照可能でしたがuseEffect
にはその機能はありませんでした。前回の props などを利用したい場合は、その情報を自分でuseRef
に保存しておくことになります。
以下の例は、自分が再レンダリングされた回数を覚えているコンポーネントです。(本当はこういう用途でuseEffect
を使わずにロジックで何とかしたほうがよいですが。というか再レンダリングされた回数が必要なのってどんな場面なのでしょうか。)
export const UseRefSample2 = () => {
const displayAreaRef = useRef();
const renderCountRef = useRef(0);
useEffect(() => {
renderCountRef.current++;
displayAreaRef.current.textContent = String(renderCountRef.current);
});
return (
<p>
このコンポーネントは
<b ref={displayAreaRef} />
回描画されました。
</p>
);
};
これと同じものをクラスコンポーネントで書くとこんな感じです。useRef
で得た ref オブジェクトがおおよそコンポーネントの(ステートではない)プロパティに対応していることが分かると思います。それにしても、やっぱりフックを用いた関数コンポーネントのほうがシンプルでいいですね(場合にもよりますが)。
export class UseRefSample2 extends React.Component {
constructor(props) {
super(props);
this.displayArea = React.createRef();
this.renderCount = 0;
}
render() {
return (
<p>
このコンポーネントは
<b ref={this.displayArea} />
回描画されました。
</p>
);
}
_effect() {
this.renderCount++;
this.displayArea.current.textContent = String(this.renderCount);
}
componentDidMount() {
this._effect();
}
componentDidUpdate() {
this._effect();
}
}
useLayoutEffect
useLayoutEffect
は useEffect
の亜種です。基本的な使い方はuseEffect
と同じですが、コールバック関数が呼ばれるタイミングが違います。具体的には、useEffect
はレンダリングの結果が描画された後に呼び出されますが、useLayoutEffect
はレンダリング結果がDOMに反映された後描画される前に呼び出されます。
レンダリング結果が描画される前にコールバックの処理が走るという特徴のため、useLayoutEffect
の処理はレンダリングをブロックし、レンダリング結果がユーザーに見えるのが遅延されます。ですから、その必要がない場合はuseEffect
を使うべきであるとされています。ちなみに、クラスコンポーネントのcomponentDidMount
やcomponentDidUpdate
のタイミングはこのuseLayoutEffect
と同じです。useEffect
は、レンダリングをブロックしなくなったという点でこれらの進化形であると言えるでしょう。
useLayoutEffect
を使う例を挙げてみます。つい先ほどのuseRef
の例を思い出してください。
export const UseRefSample2 = () => {
const displayAreaRef = useRef();
const renderCountRef = useRef(0);
useEffect(() => {
renderCountRef.current++;
displayAreaRef.current.textContent = String(renderCountRef.current);
});
return (
<p>
このコンポーネントは
<b ref={displayAreaRef} />
回描画されました。
</p>
);
};
これの返り値を見てみると、b要素の中身は最初空です。useEffect
のコールバックによってこの中身が埋められますが、ここに問題があります。useEffect
のコールバックはレンダリングが描画後に呼び出されるということは、この中身が空の状態が一瞬ユーザーに見えてしまうのです。実際、下の画像のような描画が表示されてしまいます。
useLayoutEffect
を使用するように書き換えることでこの現象を回避できます。
import * as React from 'react';
const { useLayoutEffect, useRef } = React;
export const UseLayoutEffectSample = () => {
const displayAreaRef = useRef();
const renderCountRef = useRef(0);
useLayoutEffect(() => {
renderCountRef.current++;
displayAreaRef.current.textContent = String(renderCountRef.current);
});
return (
<p>
このコンポーネントは
<b ref={displayAreaRef} />
回描画されました。
</p>
);
};
useDebugValue
これが恐らく一番新しいフックで、React Hooksを昔は追っていたという方は知らないかもしれません。その名の通り、これはデバッグに使えるフックです。具体的には、カスタムフックのデバッグ情報をReactの開発者ツール用拡張機能に表示させることができます。
これは先ほどの「レンダリング回数を覚えているコンポーネント」をベースにした例です。
import * as React from 'react';
const { useEffect, useRef, useDebugValue } = React;
const useRenderCount = () => {
const renderCountRef = useRef(0);
useDebugValue(
`このコンポーネントは${renderCountRef.current}回再描画されました`,
);
useEffect(() => {
renderCountRef.current++;
});
};
export const UseDebugValueSample = () => {
useRenderCount();
return <p>このコンポーネントを開発者ツールで見ると再描画数が表示されます</p>;
};
ここで定義されているuseRenderCount
関数はカスタムフックです。カスタムフックについては筆者の以前の記事に譲りますが、要するに名前がuse
で始まるただの関数です。普通は上の例のように、いくつかのフック呼び出しをまとめて関数にしたものがカスタムフックです。ただの関数ということは、あるコンポーネントからuseRenderCount
カスタムフックを呼び出すのは、その中身に書かれているフックを全部呼び出すのと同じです。何も特殊なことはありません。
ただ、useDebugValue
フックは自分がどのカスタムフックから呼び出されたのか検知します3。そして、対応するカスタムフックの横に指定したデバッグ情報を表示します。
実際、このUseDebugValueSample
をReact用拡張機能で見ると下の画像のように表示されています。useDebugValue
フックで指定した値がuseRenderCount
カスタムフックのデバッグ情報として表示されていることが分かります。
このフックは、これから増えるであろう、カスタムフックをライブラリで提供するような場合に便利かもしれません。
デバッグ情報の遅延計算
useDebugValue
の呼び出しはただのJavaScriptコードですから、フックの処理時には普通に実行されます。つまり、開発者ツールを使用していない一般ユーザーがアプリを実行している場合であってもuseDebugValue
の呼び出しは(実際見られることはないので無意味ですが)行われています。
凝ったデバッグ情報を表示したい場合、useDebugValue
の引数の計算が多少重い処理になるかもしれません。そのような場合、開発者ツールを使わないユーザーの処理が重くなってしまうのは望ましくありません。そのような事態を避けるために、useDebugValue
の第2引数に、生のデータをデバッグ情報に加工する関数を渡すことができます。こうすると、その関数は実際にデバッグ情報を表示する際に実行されます。これにより、デバッグ情報が必要ない場合に余計な計算を省くことができるというわけです。この機能は以下のように使います。
useDebugValue(
renderCountRef.current,
count => `このコンポーネントは${count}回再描画されました`,
);
この例では、第1引数はカウント数という生のデータになり、それを文字列へと加工する処理は遅延されるようになりました。
useImperativeHandle
これが最後のフックです。このフックは、コンポーネントのインスタンスが持つメソッドを生成することができるフックです。使い方は例を参照してください。なお、コンポーネントのインスタンスというのは、ref
で取得できるオブジェクトのことです。<div ref={myRef} />
のようにDOM要素に対してref
を使った場合は生のDOMノードが得られますが、<MyComponent ref={myRef} />
の場合はMyComponent
コンポーネントのインスタンス(のようなもの)が得られることになります。MyComponent
側がuseImperativeHandle
フックを使うことで、ここで得られるインスタンスにメソッドを生やすことができます。
次の例は、少し前に出てきた、現在時刻をリアルタイムに表示するサンプルを改造したものです。
import * as React from 'react';
const {
useEffect,
useRef,
useCallback,
useImperativeHandle,
forwardRef,
} = React;
export const UseImperativeHandleSample = () => {
const clockRef = useRef();
// スタートボタンを押したときの処理
const onStart = useCallback(() => {
clockRef.current.start();
}, []);
// ストップボタンを押したときの処理
const onStop = useCallback(() => {
clockRef.current.stop();
}, []);
return (
<div>
<Clock ref={clockRef} />
<p>
<button onClick={onStart}>再開</button>
<button onClick={onStop}>停止</button>
</p>
</div>
);
};
const Clock = forwardRef((_props, ref) => {
// 時刻を表示する場所のref
const displayAreaRef = useRef();
// リアルタイム表示がオンかどうかのref
const enabledFlagRef = useRef(true);
// リアルタイムに時刻を表示する処理
useEffect(() => {
let rafid = null;
const loop = () => {
// リアルタイム表示がオンのときのみ表示を更新
if (enabledFlagRef.current) {
// 現在時刻を表示
const now = new Date();
displayAreaRef.current.textContent = `${String(now.getHours()).padStart(
2,
'0',
)}:${String(now.getMinutes()).padStart(2, '0')}:${String(
now.getSeconds(),
).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}`;
}
rafid = requestAnimationFrame(loop);
};
loop();
return () => cancelAnimationFrame(rafid);
});
// コンポーネントのインスタンスが持つメソッドを宣言
useImperativeHandle(ref, () => ({
start() {
enabledFlagRef.current = true;
},
stop() {
enabledFlagRef.current = false;
},
}));
return <p ref={displayAreaRef} />;
});
最後なので例が少し長くなっています。下で定義されているClock
コンポーネントがuseImperativeHandle
フックを使用してstart
とstop
という2つのメソッドを宣言しています。このコンポーネントはenabledFlagRef
の値を用いて表示を更新するかどうかを制御しており、この2つのメソッドを用いてそれを外部から制御できるようにしようという魂胆です。使う側であるUseImperativeHandleSample
コンポーネントは、clockRef
にClock
コンポーネントのインスタンスを入れて、ボタンが押されるとそのstart
やstop
を呼び出すようになっています。
useImperativeHandle
を使うときの最初のポイントは、よく見るとClock
がforwardRef
という関数に包まれている点です。forwardRef
は大雑把に言えば関数型コンポーネントのref
を加工したいときに使うものです。今回はまさにuseImperativeHandle
によってref
にメソッドを生やそうとしているのでした。
forwardRef
の引数として関数コンポーネントを渡すのですが、第2引数としてref
が渡されるようになっています。これがそのままuseImperativeHandle
の第1引数となります。第2引数はオブジェクトを返す関数です。返されたオブジェクトが持っているメソッドが、そのままこのコンポーネントのref
のメソッドとなります。
名前について
Imperativeというのは「手続き的」ということです。手続き的というのは、props等によって宣言的にUIを定義するReactの流儀から外れていることを意味しています。実際、この例だってuseImperativeHandle
を使う必然性があるわけではなく、オン/オフのフラグをpropsで渡すことも可能です。そもそもコンポーネントにメソッドを生やすというのはコンポーネントをprops(やコンテキスト)以外の手段で制御しようということですから、真っ向からReactのやり方に反しているのがお分かりになると思います。それでも需要があるからこそこのフックが追加されたのでしょうが、あまり積極的に使うものでもないよというメッセージが名前に表れています。
まとめ
以上で、React Hooksの最初の正式版が導入されたReact 16.8に存在するフックを全て解説しました。個人的によく使いそうなのはuseState
とuseEffect
、あとuseRef
あたりです。useRef
はuseEffect
のロジックが複雑化してきたら出番が増えてきます。副作用を複雑化させるのはあまり褒められたことではありませんが。
途中何回かクラスコンポーネントとの比較を挟みましたが、さすが後発のAPIだけあって、よりシンプルで直感的な記述が可能になっているのがお分かりになったことでしょう。そもそも関数コンポーネント自体がシンプルなAPIということもあり、ソースコードのシンプルさに大きく貢献してくれます。また、最初関数コンポーネントで書いていたのに状態が必要になってしまったときに、クラスコンポーネントに書き直す必要がないというのも嬉しい点です4。
途中何回かリンクしましたが、筆者の他の記事にカスタムフックを取り扱ったものがあります。今回紹介したフックたちを組み合わせてカスタムフックを作ることこそReact Hooksの本質であると言っても過言ではありません。そう、これを読み終わってなるほどと思ったあなたはまだReact Hooksのスタートラインの3メートルくらい手前にいる状態なのです。ぜひこちらの記事も読んでReact Hooksのスタートを切ってください。
-
一応補足しておくと、クラスコンポーネントなどの旧来の機能が React Hooks に取って代わられて廃止されるという予定は今のところありません。ですから、React Hooks を避けながら React を使い続けることも可能です。筆者はそういう人(/チーム)は React を使うのに向いていないという説を推しますが。 ↩
-
第 2 引数の省略と、第 2 引数に
[]
を指定するのとは異なるという点に注意してください。第 2 引数を省略した場合はレンダリングごとにコールバック関数が呼ばれますが、[]
を指定した場合は初回のレンダリングでのみコールバック関数が呼ばれます。 ↩ -
ちゃんと調べたわけではありませんが、どうやら
Error
オブジェクトを生成してコールスタックを入手しているようです。 ↩ -
recompose
を使っている人は関数コンポーネントのままでもいけるぞと思ったかもしれません。それはある意味で正しく、React Hooksはrecompose
の進化系と考えることもできます。なお、recompose
はReact Hooksの登場と同時に機能追加等の停止が宣言されました。今後はReact Hooksがrecompose
に取って代わることになります。 ↩