🎬

Reactのフックについてまとめてみた

2022/01/14に公開

フックとは

stateやライフサイクルの機能などを、関数コンポーネント内で使用できるようにするための関数です。コンポーネント間でのロジックの再利用ができます。
データが変更されると、フックは自身がフックされたコンポーネントを新しいデータで再描画する機能を持ちます。

ライフサイクルのおさらい

React のコンポーネントがレンダリングされるタイミングは以下の3つです。

  1. マウント(DOM の初回ロード)された時
  2. コンポーネントが更新された時
  3. アンマウントされた時

※公式のライフサイクル図も参考にして下さい。

useState

useStateは関数コンポーネントに対して、stateの保持と更新をするための機能を追加します。
stateの初期値を受け取って配列を返す関数です。
配列の先頭の要素には、ステートの現在値が格納されています。初回のレンダー時に返されるstateは、useStateに第一引数に指定した値になります。
配列の二番目の要素には、コンポーネント内からステート値を変更する関数が格納されています。

使い方

const [state, stateを更新する関数 ] = useState(初期値);

const updateFunction = () => {
    stateを更新する関数();
};

useContext

コンテクストオブジェクト(React.createContext からの戻り値)を受け取り、そのコンテクストの現在値を返します。コンテクストの現在値は、ツリー内でこのフックを呼んだコンポーネントの直近にある <MyContext.Provider> のvalueの値によって決定されます。

<MyContext.Provider> が更新された場合、このフックはその MyContext プロバイダに渡された最新の value の値を使って再レンダーを発生させます。

コンテクストとは

コンテクストを使用することで、ステートを1箇所で管理する事ができ、中間のコンポーネントを経由する必要がなくなります。
コンテクストを利用するには、まずコンポーネントプロバイダーにデータを渡します。
コンポーネントプロバイダーは、コンポーネントツリー全体もしくは一部を囲みます。
データの終着地となるのが、コンテクストコンシューマーです。コンテクストコンシューマーは、コンテクストからデータを読み出すのに使われます。

使い所

コンポーネントの階層が深い場合、Globalなデータ共有(propsのバケツリレー)をシンプルに実装したいケースが一般的です。

index.js
import React, { createContext } from "react";
// ... 略

// コンテクストオブジェクトを作成。ProviderとConsumerの2つのコンポーネントを含む
export const MyContext = createContext();

render(
    // アプリケーションのデータを渡して、App配下のすべてのコンポーネントから参照可能になる
    <MyContext.Provider value={ value }>
        <App />
    </MyContext.Provider>,
    documents.getElementById("root");
)
getDate.js
import React, { useContext } from "react";
import { MyContext } from './index';
// ... 略

// useContextは内部でコンテクストのConsumerコンポーネントを使用してデータを取得します
const value = useContext(MyContext);

コンテキストとステートの併用

コンテキストプロバイダーを使えば、どのコンポーネントからもコンテキストからデータの取得が可能になります。
しかし、データを変更する事ができません。
ステートを保持し、コンテキストプロバイダーを描画するコンポーネントをカスタムプロバイダーと呼びます。
カスタムプロバイダーの例である、ColorProviderを見てみます。

役割

  • ColorProvider.jsx → カスタムプロバイダー
  • Top.jsx → ルートコンポーネント
  • Menu.jsx → stateを取得する、下層コンポーネント
ColorProvider.jsx
import React, { createContext, useState } from "react";
import colorData from "./color-data.json";

// defaultValueの設定
const themes = {
  colors: [
    {id: "1", color: "yellow", rating: 3}
  ],
  rateColor: () => {}
};

const ColorContext = createContext(themes);
export const useColors = () => useContext(ColorContext);

export default function ColorProvider({ children }) {
  const [colors, setColors] = useState(colorData);

  const rateColor = (id, rating) =>
    setColors(
      colors.map(color => (color.id === id ? { ...color, rating } : color))
    );

  return (
    <ColorContext.Provider value={{ colors, rateColor }}>
      {children}
    </ColorContext.Provider>
  );
}
  • ColorProviderは、ColorContext.Provider(コンテキストプロバイダー)を描画するコンポーネントです
  • createContextは、初期値を設定する
  • useStateフックを用いて、アプリケーション全体のデータcolorsをステート値として保持しています
  • colorsをColorContext.Providerコンポーネントのvalueプロパティに設定しています
  • useContextを使用する事で、children配下の全てのコンポーネントで参照可能です
  • useColorsフックを用意し、コンテキストはカスタムフック経由でアクセス可能になります
  • setColorsの関数を呼ぶ事で、新しいcolorsの値でコンポーネントツリーが再描画(※)されます
Top.jsx
import ColorProvider from "/@/ColorProvider";
import Component from "/@/Component";

const Top = () => {
  return (
    <ColorProvider>
      <Component/>
    </ColorProvider>
  );
};

export default Top;
  • ColorProviderコンポーネントで囲むと、下層コンポーネントでcontextに設定されたデータなどの取得が可能になります
Menu.jsx
import Item from "/@/Item";
import { useColors } from "/hook/colorProvider";

const Menu = () => {
  const {colors} = useColors();
  return(
    <div>
      {colors.map( (color, i) => {
        return <Item key={i} {...color}/>;
      })}
    </div>
  );
};

export default Menu;
  • useColorsフックは、state値を取得します

useEffect

コンポーネントの描画が完了した後に呼び出されます。useEffectは、コールバック関数を引数にとるので、副作用として実行したい処理を記述します。
副作用とは、APIでのデータ取得やコンポーネント内のDOM要素の手動での変更などを指します。
useEffectフックは、ステート管理のフックと協調して働くように設計されています。ステートが更新され、コンポーネントツリーが再描画された後。最終的にuseEffectフックに設定された副作用関数が実行されます。

useEffect(() => {
    /* 第1引数には実行させたい副作用関数を記述*/
    console.log('レンダーされました。');
});

依存配列

依存配列は、useEffectの2番目の引数として渡される配列です。副作用関数の実行タイミングを制御する依存データを設定できます。例えば、useEffectの第2引数に[count]を渡すと、countに変化があった時だけ副作用関数を実行します。
また、依存配列に空の配列を指定した場合は、コンポーネントの初回描画時のみ副作用が実行されます。

useEffect(() => {
    console.log('レンダーされた、直後です。');
},[依存する変数の配列]); // 第2引数には副作用関数の実行タイミングを制御する依存データを記述

クリーンアップ処理

useEffectのコールバック関数は戻り値を記述する事もできます。
その場合、コンポーネントがアンマウントされた時に、retrun文が実行されます。
この仕様を利用し、クリーンアップ処理を定義できます。
使い所としては不要な副作用の削除です、イベントリスナの削除、タイマーのキャンセルなどが挙げられます。

useEffect(() => {
    const  interval = setInterval(time, 1000);
    // クリーンアップ処理
    return () => {
      clearInterval(interval);
      console.log('cleared');
    }
 }, []);

パフォーマンスの最適化

useEffect内に記述された副作用は、コンポーネントのレンダー毎に呼ばれます。
その為、依存配列を以下の用に活用し、パフォーマンスの最適化を図る必要があります。

  • 空配列 ([]) を記述し、コンポーネントの初回描画時のみ副作用が実行されるようにする
  • 依存する変数の配列を記述し、副作用関数の実行タイミングを制御する

useLayoutEffect

ほとんどの場合、useEffectで事足りるのですが、描画が画面に反映される手前で何か処理を実行したい場合にuseLayoutEffectを使います。
useEffectとの呼び出しの違いを分かりやすく順番で明記します。

  1. コンポーネントの描画関数が呼び出される。
  2. useLayoutEffectで設定した副作用関数が呼び出される
  3. コンポーネントの描画結果が画面に反映される
  4. useEffectで設定した副作用関数が呼び出される
useLayoutEffect(() => {
    console.log('レンダーされた、直前です。');
},[依存する変数の配列]); // 第2引数には副作用関数の実行タイミングを制御する依存データを記述

useMemo

useMemoは関数の結果を保持するためのフックで、何回やっても結果が同じ場合の値などを保存(メモ化)し、保存したものから値を再取得します。
メモ化とは、パフォーマンス最適化のために計算結果をキャッシュすることを指します。
useMemoは関数と依存配列を引数に取ります。関数は依存配列のいずれかの値が変わった場合のみ呼び出され、戻り値はキャッシュされます。

依存配列の設定

  • 依存配列を空で渡す場合
    ・初期描画のレンダリングのみ、処理が実行されます。
    ・初期に保存された値が常に再利用されるようになります。
  • 依存配列に値を入れる場合
    ・依存配列に指定した値が更新された時に処理を実行します。
import React, { useMemo } from 'react';
  
const memorizedVal = useMemo(() => {
  calculateDouble(inputDouble);
}, [num]);

useCallback

useCallbackも、useMemoと同様にパフォーマンス向上のためのフックで、メモ化したコールバック関数を返します。

// ... 略
import React, { useCallback } from 'react';
  
const memorizedCallback = useCallback(() => {
  callbackFunc(value);
}, [value]);

useReducer

useReducerは、状態管理のためのフックで、useStateと似たような機能を持ちます。
加えて、state更新のロジックを抽象化する事が出来ます。
useReducerは、reducerの関数とstateの初期値を引数に取ります。reducerはstateを更新するための関数です。useReducerの戻り値の配列は、1番目の要素がstate値で、2番目の要素がreducerを実行するための関数(dispatch関数)となります。
useReducerの使い所としては、複数の値にまたがる複雑なstateのロジックがある場合や、前のstateに基づいて次のstateを決める必要がある場合です。

// reducerはstateを更新するための関数、dispatchは、reducerを実行するための呼び出し関数
const [state, dispatch] = useReducer(reducer, initialState)
function User() {
  // useReducerによりステートを管理する
  const [user, setUser] = useReducer(
    (user, newDetails) => ({ ...user, ...newDetails }),
    firstUser
  );

  return (
    <>
      <button
        onClick={() => {
          setUser({ admin: true }); // 変更されたプロパティのみを指定する
        }}
      >
        Make Admin
      </button>
    </>
  );
}

useSelector

storeに保存されているstateデータの中から必要なデータを選択して取り出すことができるフックです。
引数に渡した関数はstateを受け取るので、取り出したい値を返してください。
actionがdispatchされると実行されます。
Redux導入の際に使用します。

import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
  const counter = useSelector(state => state.counter)
  return <div>{counter}</div>
}

useDispatch

storeからdispatch関数を参照し、返り値として返します。
actionをdispatchするために使用します。
storeを変更しない限り、返り値のdispatch関数は変更されません。

import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'increment-counter' })}>
        Increment counter
      </button>
    </div>
  )
}

カスタムフック(use*)

カスタムフックは複数のコンポーネントに存在する共通の処理を取り出して作成した関数です。
汎用的なカスタムフックを作成することで、アプリケーション内で再利用する事が出来ます。
カスタムフックの命名規則としては、小文字のuseの後に、任意のHook名を繋げます。
(例)useInput

汎用的なカスタムフックを集めたRooksといったサイトもあるので活用して下さい。

useInput.js
import { useState } from 'react'

// 入力値が変わる度にstateを変更する処理
export const useInput = (initialValue) => {
  const [value, setValue] = useState(initialValue);
  return [
    { value , onChange: e=> setValue(e.target.value) },
    () => setValue(initialValue);
  ]
}

フックの使い方のルール

  1. フックはコンポーネントのスコープで実行すること
    フックは関数コンポーネントもしくはカスタムフックの中から呼び出されなければいけません。
  2. ひとつのフックで多くのことをせず複数のフックに分割すること
    コードが読みやすくなる事と、処理の実行順を指定できるという利点があるのが理由です。
  3. フックは常に描画関数のトップレベルから呼び出されないといけない
    フックを条件文やループ、もしくはネストした関数から呼び出す事は出来ません。

Discussion