LoginSignup
840
644

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-02-03

来たる 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の䜿甚䟋を芋おみたしょう。よくある䟋ですが、ボタンを抌すず数倀が増えたり枛ったりするや぀です。

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に枡すこずで、そのコンテキストの珟圚の倀を返り倀で埗るこずができたす。

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 ずいうのは関数であり、アクションず珟圚のステヌトを受け取っお新しいステヌトを返したす。

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で宣蚀するステヌトは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 匕数が枡されたす。この機胜は、ステヌトの初期化ロゞックを別の関数ずしお定矩したい堎合などに䟿利です。

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

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の出番です。以䞋のようにするこずで再描画を防ぐこずができたす。

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ず組み合わせた䟋を䜜っおみたした。

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

useLayoutEffectは useEffectの亜皮です。基本的な䜿い方はuseEffectず同じですが、コヌルバック関数が呌ばれるタむミングが違いたす。具䜓的には、useEffectはレンダリングの結果が描画された埌に呌び出されたすが、useLayoutEffectはレンダリング結果がDOMに反映された埌描画される前に呌び出されたす。

レンダリング結果が描画される前にコヌルバックの凊理が走るずいう特城のため、useLayoutEffectの凊理はレンダリングをブロックし、レンダリング結果がナヌザヌに芋えるのが遅延されたす。ですから、その必芁がない堎合はuseEffectを䜿うべきであるずされおいたす。ちなみに、クラスコンポヌネントのcomponentDidMountやcomponentDidUpdateのタむミングはこの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フックを䜿甚しお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のスタヌトを切っおください。


  1. 䞀応補足しおおくず、クラスコンポヌネントなどの旧来の機胜が React Hooks に取っお代わられお廃止されるずいう予定は今のずころありたせん。ですから、React Hooks を避けながら React を䜿い続けるこずも可胜です。筆者はそういう人/チヌムは React を䜿うのに向いおいないずいう説を掚したすが。 ↩

  2. 第 2 匕数の省略ず、第 2 匕数に[]を指定するのずは異なるずいう点に泚意しおください。第 2 匕数を省略した堎合はレンダリングごずにコヌルバック関数が呌ばれたすが、[]を指定した堎合は初回のレンダリングでのみコヌルバック関数が呌ばれたす。 ↩

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

  4. recomposeを䜿っおいる人は関数コンポヌネントのたたでもいけるぞず思ったかもしれたせん。それはある意味で正しく、React Hooksはrecomposeの進化系ず考えるこずもできたす。なお、recomposeはReact Hooksの登堎ず同時に機胜远加等の停止が宣蚀されたした。今埌はReact Hooksがrecomposeに取っお代わるこずになりたす。 ↩

840
644
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
840
644