16.React|フック

フックの理解はReactを使っている限りマストだと思うが、まだしっかりと理解できているとは言えないので随時整理しつつ理解を深めていきたい。

まずフックとは何か

フック (hook) は React 16.8 で追加された新機能です。state などの React の機能を、クラスを書かずに使えるようになります。

フックとは、関数コンポーネントに state やライフサイクルといった React の機能を “接続する (hook into)” ための関数です。フックは React をクラスなしに使うための機能ですので、クラス内では機能しません。

ja.reactjs.org

フックのルール

フックは関数のトップレベルのみで呼び出してください。ループや条件分岐やネストした関数の中でフックを呼び出さないでください。

フックは React の関数コンポーネントの内部のみで呼び出してください。通常の JavaScript 関数内では呼び出さないでください(ただしフックを呼び出していい場所がもう 1 カ所だけあります — 自分のカスタムフックの中です。)。

useEffect/副作用フック

Reactのコンポーネント内部でDOMを更新したり、外部データの取得を行なったりすることを「副作用フック」と呼ぶ。
これらの操作は他のコンポーネントに影響を及ぼすことがあり、またレンダーの最中に実行することができない。
そのため「副作用(side-effects)」または、省略して「作用(effects)」と呼ぶ。

「useEffect」は、下記のように副作用フックを可能にしてくれる。
count Stateを「useState」で定義し、更にそれをタイトルに反映する「useEffect」を定義している。

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

DOM の書き換え、データの購読、タイマー、ロギング、あるいはその他の副作用を、関数コンポーネントの本体(React のレンダーフェーズ)で書くことはできません。それを行うと UI にまつわるややこしいバグや非整合性を引き起こします。

代わりに useEffect を使ってください。useEffect に渡された関数はレンダーの結果が画面に反映された後に動作します。副作用とは React の純粋に関数的な世界から命令型の世界への避難ハッチであると考えてください。

デフォルトでは副作用関数はレンダーが終了した後に毎回動作しますが、特定の値が変化した時のみ動作させるようにすることもできます。

エフェクトのクリーンアップ

副作用はしばしば、コンポーネントが画面から消える場合にクリーンアップする必要があるようなリソース(例えば購読やタイマー ID など)を作成します。これを実現するために、useEffect に渡す関数はクリーンアップ用関数を返すことができます。例えば、データ購読を作成する場合は以下のようになります。

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // Clean up the subscription
    subscription.unsubscribe();
  };
});

メモリリークを防止するため、コンポーネントが UI から削除される前にクリーンアップ関数が呼び出されます。それに加えて、コンポーネントが複数回レンダーされる場合(大抵はそうですが)、新しい副作用を実行する前に前回の副作用はクリーンアップされます。この例では、更新が発生する度に新しい購読が作成される、ということです。

条件付きで副作用を実行する

第2引数が渡されていないことによって更新された情報が検知されないバグがあったので、改めて学習。

useEffect(effect [, dependencies]);

デフォルトの動作では、副作用関数はレンダーの完了時に毎回実行されます。これにより、コンポーネントの依存配列のうちのいずれかが変化した場合に毎回副作用が再作成されます。

しかし、上述のデータ購読の例でもそうですが、これは幾つかのケースではやりすぎです。新しい購読を設定する必要があるのは毎回の更新ごとではなく、source プロパティが変化した場合のみです。

これを実装するためには、useEffect の第 2 引数として、この副作用が依存している値の配列を渡します。変更後の例は以下のようになります。

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

これで、データの購読は props.source が変更された場合にのみ再作成されるようになります。

空の配列 [] を渡すと、この副作用がコンポーネント内のどの値にも依存していないということを React に伝えることになります。つまり副作用はマウント時に実行されアンマウント時にクリーンアップされますが、更新時には実行されないようになります。

※この「更新時の実行」というのが肝。

ja.reactjs.org

danburzo.github.io

useMemo

メモ化された値を返す。
レンダリング中に第一引数で渡した関数を実行し、実行結果をキャッシュしてくれる。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

“作成用” 関数とそれが依存する値の配列を渡してください。useMemo は依存配列の要素のいずれかが変化した場合にのみメモ化された値を再計算します。この最適化によりレンダー毎に高価な計算が実行されるのを避けることができます。

useMemo に渡した関数はレンダー中に実行されるということを覚えておいてください。レンダー中に通常やらないようなことをこの関数内でやらないようにしましょう。例えば副作用は useMemo ではなく useEffect の仕事です。

useMemo はパフォーマンス最適化のために使うものであり、意味上の保証があるものだと考えないでください。将来的に React は、例えば画面外のコンポーネント用のメモリを解放するため、などの理由で、メモ化された値を「忘れる」ようにする可能性があります。useMemo なしでも動作するコードを書き、パフォーマンス最適化のために useMemo を加えるようにしましょう

function MyList(list, query) {
  // On every component render it will be refiltered
  const filteredList = filterListByQyery(list, query);

  // Will recalculate only when the list or the query changes
  const memoizedFilteredList = React.useMemo(
    () => filterListByQyery(list, query),
    [list, query],
  );
}

dev.to