🎉React 16.8: 正式版となったReact Hooksを今さら総ざらいする

来たる 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パッケージからuseStateuseEffectなどの関数がエクスポートされています。裏では React が今どのコンポーネントを処理しているかといった情報を保持しており、これらの関数はその情報に基づいて適切に動作します。現在のところ、React Hooks の API は関数コンポーネント専用であり、それ以外のシチュエーションで呼び出すことはできません。

基本のフック

先述のドキュメントでは、useState, useEffect, useContextの 3 つが基本的なフックとして挙げられています。それに倣ってここでもまずはこの 3 つを紹介します。

useState

useStateフックは、関数コンポーネントにステートを持たせられる API です。このフックを使うと、クラスコンポーネントにおけるthis.stateに値を保存しておくのと同じような感じで、コンポーネントに状態を持たせることができます。使い方はconst [stateの値, state更新関数] = useState(state初期値);というのが基本です。

useState のサンプル

さっそくですが、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>
  );
};

この例ではコンポーネントはleftrightという 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に渡すことで、そのコンテキストの現在の値を返り値で得ることができます。

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

useReduceruseState の亜種であり、ステートを宣言するフックです。useReducerは、ステートの初期状態に加えてreducer と呼ばれる関数を渡します。結果として、現在のステートに加えて、ステートを更新する関数の代わりに dispatch 関数が得られます。この reducer や dispatch というのは Redux 用語としてよく知られていますので、Redux を触ったことがあるかたは馴染み深い概念でしょう。

useReducerによって作られたステートを更新する場合は、dispatch関数にアクションを渡します。アクションというのはステートの更新指示を表す値で、別になんでも構いません。そのアクションを引数として、useReducerに渡した reducer が呼び出されます。reducer というのは関数であり、アクションと現在のステートを受け取って新しいステートを返します。

useReducerの例
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>
  );
};

この例はこのようにレンダリングされます。

Image from Gyazo

この例のように、useReducerには 2 つの引数を渡します。1 つ目の引数は reducer で、2 つ目の引数は初期状態です。今回useReducerで宣言するステートはyearmonthという 2 つのプロパティを持ったオブジェクトです。+ボタンと-ボタンを押すと、月が 1 つ増えます。12 ヶ月になると年が 1 つ増えて月が 0 に戻ります。(あくまでサンプルなので、年とか 12 で割って計算しろよという批判は受け付けません。)

ボタンが押されたときのロジックがreducer関数にまとまっていることが分かりますね。reducerの第 1 引数が今の状態で、第 2 引数がアクション(単純な例なので今回は文字列)です。そして、useReducerにより宣言されたステートの更新は、dispatch関数にアクションを渡すことで行います。dispatchを子コンポーネントであるControlButtonsに渡すのは先ほど紹介したuseContextを使いました。

今回のポイントは、+ボタンを押すとyearmonthが両方同時に変化する可能性があるということです。このような複雑な変化をアクションという抽象的な命令で表すことによって、ステートを変化させる側(dispatchを呼び出す側)の単純化とステート変更ロジックの分離を同時に達成しています。useReducerを用いて色々な状態をひとまとめに宣言する場合、色々な子コンポーネントがdispatchを用いて状態を変化させるはずですから、この例のようにuseContextと組み合わせてdispatch関数を子コンポーネントたちに伝えるのが特に適しています。ステートの更新はすべてdispatchを通じて行うため、dispatch関数ひとつ伝えれば十分であるというのも嬉しい点です。

関数によるステートの初期化

useReducerの第 2 引数に初期ステートを渡す代わりに、関数を用いてステートを初期化することができます。この場合、useReducerの第 3 引数に初期化関数を渡します。また、第 2 引数の意味が変わります。渡した初期化関数の引数としてuseReducer第 2 引数が渡されます。この機能は、ステートの初期化ロジックを別の関数として定義したい場合などに便利です。

useReducerの第3引数の例
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 引数に計算される値が依存する値の一覧を渡します。

useMemoの例
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

useCallbackuseMemoの亜種です。簡潔に言えば、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の出番です。以下のようにすることで再描画を防ぐことができます。

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は再描画されなくなります。

ただし、IncrementButtonReact.memoで囲うように変更されている点に注意してください。これはフックではないので詳細は省きますが、親のUseCallbackSampleが再描画されても自動的にIncrementButtonが再描画しないようにする効果があります。この 2 つの施策によりIncrementButtonが再描画される要因が無くなります。このようにuseCallback(やuseMemo)は最適化に利用することができます。

useRef

useRefは、ref オブジェクトを作って返すフックです。この ref オブジェクトはReact.createRefを使って作ることができるオブジェクトのことです。useRefは、同じ呼び出しに対しては同じ ref オブジェクトを返します。ref オブジェクトはcurrentプロパティにその中身が入っています。currentの初期値はuseRefの引数で指定することができます。

useRefのひとつの用途は、コンポーネントのref属性に渡すためのrefオブジェクトを作ることです。useEffectと組み合わせた例を作ってみました。

useRefのサンプル
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} />;
};

このコンポーネントはこのような表示となります。

Image from Gyazo

これは、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

useLayoutEffectuseEffectの亜種です。基本的な使い方はuseEffectと同じですが、コールバック関数が呼ばれるタイミングが違います。具体的には、useEffectはレンダリングの結果が描画された後に呼び出されますが、useLayoutEffectはレンダリング結果がDOMに反映された後描画される前に呼び出されます。

レンダリング結果が描画される前にコールバックの処理が走るという特徴のため、useLayoutEffectの処理はレンダリングをブロックし、レンダリング結果がユーザーに見えるのが遅延されます。ですから、その必要がない場合はuseEffectを使うべきであるとされています。ちなみに、クラスコンポーネントのcomponentDidMountcomponentDidUpdateのタイミングはこのuseLayoutEffectと同じです。useEffectは、レンダリングをブロックしなくなったという点でこれらの進化形であると言えるでしょう。

useLayoutEffectを使う例を挙げてみます。つい先ほどのuseRefの例を思い出してください。

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のコールバックはレンダリングが描画後に呼び出されるということは、この中身が空の状態が一瞬ユーザーに見えてしまうのです。実際、下の画像のような描画が表示されてしまいます。

Image from Gyazo

useLayoutEffectを使用するように書き換えることでこの現象を回避できます。

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の開発者ツール用拡張機能に表示させることができます

これは先ほどの「レンダリング回数を覚えているコンポーネント」をベースにした例です。

useDebugValueの例
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カスタムフックのデバッグ情報として表示されていることが分かります。

Image from Gyazo

このフックは、これから増えるであろう、カスタムフックをライブラリで提供するような場合に便利かもしれません。

デバッグ情報の遅延計算

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フックを使うことで、ここで得られるインスタンスにメソッドを生やすことができます。

次の例は、少し前に出てきた、現在時刻をリアルタイムに表示するサンプルを改造したものです。

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フックを使用してstartstopという2つのメソッドを宣言しています。このコンポーネントはenabledFlagRefの値を用いて表示を更新するかどうかを制御しており、この2つのメソッドを用いてそれを外部から制御できるようにしようという魂胆です。使う側であるUseImperativeHandleSampleコンポーネントは、clockRefClockコンポーネントのインスタンスを入れて、ボタンが押されるとそのstartstopを呼び出すようになっています。

useImperativeHandleを使うときの最初のポイントは、よく見るとClockforwardRefという関数に包まれている点です。forwardRefは大雑把に言えば関数型コンポーネントのrefを加工したいときに使うものです。今回はまさにuseImperativeHandleによってrefにメソッドを生やそうとしているのでした。
forwardRefの引数として関数コンポーネントを渡すのですが、第2引数としてrefが渡されるようになっています。これがそのままuseImperativeHandleの第1引数となります。第2引数はオブジェクトを返す関数です。返されたオブジェクトが持っているメソッドが、そのままこのコンポーネントのrefのメソッドとなります。

名前について

Imperativeというのは「手続き的」ということです。手続き的というのは、props等によって宣言的にUIを定義するReactの流儀から外れていることを意味しています。実際、この例だってuseImperativeHandleを使う必然性があるわけではなく、オン/オフのフラグをpropsで渡すことも可能です。そもそもコンポーネントにメソッドを生やすというのはコンポーネントをprops(やコンテキスト)以外の手段で制御しようということですから、真っ向からReactのやり方に反しているのがお分かりになると思います。それでも需要があるからこそこのフックが追加されたのでしょうが、あまり積極的に使うものでもないよというメッセージが名前に表れています。

まとめ

以上で、React Hooksの最初の正式版が導入されたReact 16.8に存在するフックを全て解説しました。個人的によく使いそうなのはuseStateuseEffect、あとuseRefあたりです。useRefuseEffectのロジックが複雑化してきたら出番が増えてきます。副作用を複雑化させるのはあまり褒められたことではありませんが。

途中何回かクラスコンポーネントとの比較を挟みましたが、さすが後発のAPIだけあって、よりシンプルで直感的な記述が可能になっているのがお分かりになったことでしょう。そもそも関数コンポーネント自体がシンプルなAPIということもあり、ソースコードのシンプルさに大きく貢献してくれます。また、最初関数コンポーネントで書いていたのに状態が必要になってしまったときに、クラスコンポーネントに書き直す必要がないというのも嬉しい点です4

途中何回かリンクしましたが、筆者の他の記事にカスタムフックを取り扱ったものがあります。今回紹介したフックたちを組み合わせてカスタムフックを作ることこそReact Hooksの本質であると言っても過言ではありません。そう、これを読み終わってなるほどと思ったあなたはまだReact Hooksのスタートラインの3メートルくらい手前にいる状態なのです。ぜひこちらの記事も読んでReact Hooksのスタートを切ってください。


  1. 一応補足しておくと、クラスコンポーネントなどの旧来の機能が React Hooks に取って代わられて廃止されるという予定は今のところありません。ですから、React Hooks を避けながら React を使い続けることも可能です。筆者はそういう人(/チーム)は React を使うのに向いていないという説を推しますが。 

  2. 第 2 引数の省略と、第 2 引数に[]を指定するのとは異なるという点に注意してください。第 2 引数を省略した場合はレンダリングごとにコールバック関数が呼ばれますが、[]を指定した場合は初回のレンダリングでのみコールバック関数が呼ばれます。 

  3. ちゃんと調べたわけではありませんが、どうやらErrorオブジェクトを生成してコールスタックを入手しているようです。 

  4. recomposeを使っている人は関数コンポーネントのままでもいけるぞと思ったかもしれません。それはある意味で正しく、React Hooksはrecomposeの進化系と考えることもできます。なお、recomposeはReact Hooksの登場と同時に機能追加等の停止が宣言されました。今後はReact Hooksがrecomposeに取って代わることになります。 

uhyo
Metcha yowai software engineer
https://uhy.ooo/
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
コメント
この記事にコメントはありません。
あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした