Open10

React Hooksのまとめ

10000leaves10000leaves

React Hooksとは

React Hooksはv16.8から追加された新機能。

Reactではクラスコンポーネントと関数コンポーネントの2種類がありましたが、v16.8以前では関数コンポーネントでは状態(state)を持つことができなかったため、クラスコンポーネントが主流でした。

v16.8からHooksの追加され、関数コンポーネントに状態を持たせることが可能になり、UIを切り替えたり、外部APIからデータを取得することができるようになりました。

現在は、Hooksを使った関数コンポーネントが主流。

参考

10000leaves10000leaves

React Hooksの種類

分類 フック名 効果 対象 使用目的
Basic Hooks useState コンポーネントの状態を管理する すべてのコンポーネント コンポーネント内で状態を保持し、その状態の変更や取得が可能になる
useEffect コンポーネントの副作用を処理する すべてのコンポーネント コンポーネントがマウントされたり更新されたりした際に副作用(API呼び出し、イベントリスナーの登録など)を実行する
useContext コンテキストを利用する すべてのコンポーネント コンテキストを利用してコンポーネントツリー内で値を共有する
Additional Hooks useReducer 複雑な状態やアクションに基づく状態の更新を処理する すべてのコンポーネント 状態の更新に基づくアクションを処理し、新しい状態を返す
useCallback コールバック関数をメモ化して再レンダリングの最適化を行う すべてのコンポーネント コールバック関数の再生成を防止し、パフォーマンスを向上させる
useMemo 値をメモ化して再レンダリングの最適化を行う すべてのコンポーネント 計算結果をキャッシュして再レンダリングの最適化を行う
useRef 参照を作成する すべてのコンポーネント コンポーネントのレンダリング間で値を保持する
useImperativeHandle リフォワードリファレンスをカスタムする 特定のコンポーネント 子コンポーネントのインスタンスを親コンポーネントで直接操作できるようにする
useLayoutEffect ブラウザの描画後に副作用を処理する すべてのコンポーネント useEffectと似ているが、ブラウザの描画後に副作用を処理するため、描画の前にDOMを直接操作する場合に適している
useDebugValue カスタムフックにラベルを付けてデバッグを補助する カスタムフック カスタムフックの値をデバッグするために使用する
useDeferredValue 遅延評価された値を扱う すべてのコンポーネント 遅延評価された値(非同期の値など)を取り扱うために使用する
useTransition 遅延ローディングやアニメーションを処理する すべてのコンポーネント コンポーネントの表示を遅延させたり、アニメーションを制御するために使用する
useId ユニークなIDを生成する すべてのコンポーネント ユニークなIDを生成し、要素の一意性を確保する
Library Hooks useSyncExternalStore 外部ストア(状態管理ライブラリなど)と同期する すべてのコンポーネント 外部の状態管理ライブラリ(Reduxなど)とReactコンポーネントを同期させる
useInsertionEffect DOMへの要素の挿入や削除を監視し、副作用を処理する すべてのコンポーネント 要素がDOMに挿入または削除されたときに副作用を実行する
10000leaves10000leaves

再レンダリングが起きる条件

再レンダリングが起きる3つのパターン

  1. stateが更新されたコンポーネント
  2. propsが変更されたコンポーネント
  3. 再レンダリングされたコンポーネント配下のコンポーネントすべて

上記のstateやpropsなどの値をReact Hooksの第二引数に指定することで、値が変化したときだけレンダリングをさせることができる

10000leaves10000leaves

useState

const [state, setState] = useState(initialState);

ステートフルな値と、それを更新するための関数を返します。

初回のレンダー時に返される state は第 1 引数として渡された値 (initialState) と等しくなります。

setIndex 関数は state を更新するために使用します。新しい state の値を受け取り、コンポーネントの再レンダーをスケジューリングします。

const newState= 1;
setState(newState);

後続の再レンダー時には、useState から返される 1 番目の値は常に、更新を適用した後の最新版の state になります。

useStateとは

useState()は、関数コンポーネントでstateを管理(stateの保持と更新)するためのReactフックであり、最も利用されるフックです。

stateとはコンポーネントが内部で保持する「状態」のことで、画面上に表示されるデータ等、アプリケーションが保持している状態を指しています。stateはpropsと違い後から変更することができます。

関数型の更新

新しい state が前の state に基づいて計算される場合は、setState に関数を渡すことができます。この関数は前回の state の値を受け取り、更新された値を返します。以下は、setState の両方の形式を用いたカウンタコンポーネントの例です。

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

スプレッド構文

スプレッド構文とはJavaScriptの機能です。
...変数がスプレッド構文にあたり、配列やオブジェクトを展開し、値渡しをする際に使います。

// Array
const odd = [1, 3]
const even = [2, 4]
const numbers = [...odd, ...even]
console.log(numbers) // [1, 3, 2, 4]

// Object
const name = {first: "Tanaka", last: "Taro"}
const age = {age: 27}
const profile = {...name, ...age}
console.log(profile) // {first: "Tanaka", last: "Taro", age: 27}

React では以下のように使用すると、インプットで送られた updatedValues と state で管理している既存の prevState がマージされます。

const [state, setState] = useState({});
setState(prevState => {
  // Object.assign would also work
  return {...prevState, ...updatedValues};
});

別の選択肢としては useReducer があります。

10000leaves10000leaves

useEffect

useEffect(didUpdate);

副作用を有する可能性のある命令型のコードを受け付けます。

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

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

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

useEffectとは

useEffectを使うと、useEffectに渡された関数はレンダーの結果が画面に反映された後に動作します。
つまりuseEffectとは、「関数の実行タイミングをReactのレンダリング後まで遅らせるhook」です。

副作用の処理(DOMの書き換え、変数代入、API通信などUI構築以外の処理)を関数コンポーネントで扱えます。
今時、クラスコンポーネントは使わないですが、クラスコンポーネントでのライフサイクルメソッドに当たります。

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

副作用を実行、制御するためにuseEffectを利用する

useEffect()の基本構文は以下の通りです。関数コンポーネントのトップレベルで宣言します。

useEffect(() => {
  /* 第1引数:実行させたい副作用関数を記述 */
  console.log('副作用関数が実行されました!')
},[依存する変数の配列]) // 第2引数:副作用関数の実行タイミングを制御する依存データを記述

第2引数を指定することにより、第1引数に渡された副作用関数の実行タイミングを制御することができます。Reactは第2引数の依存配列の中身の値を比較して、副作用関数をスキップするかどうかを判断します。

説明 データ型
第1引数 副作用関数(戻り値はクリーンアップ関数、または何も返さない) 関数
第2引数 副作用関数の実行タイミングを制御する依存データが入る(省略可能) 配列

初回レンダリング時のみ副作用関数を実行させる

副作用関数を初回レンダリング時の一度だけ実行させたい場合、第2引数に空の依存配列[]を指定します。
この場合、初回レンダリング時のみ副作用関数が実行されます。

  useEffect(() => {
    console.log('初回レンダリング')
  },[])

依存配列の値が変化した場合のみ副作用関数を実行させる

useEffect()の第2引数に[count]を渡すと、countに変化があったときだけ副作用関数を実行します。

  useEffect(() => {
    console.log(count)
    console.log('再レンダーされました')
  },[count])
10000leaves10000leaves

useContext

useContext はコンポーネントでコンテクスト (Context) の読み取りとサブスクライブ(subscribe, 変更の受け取り)を行うための React フックです。

const value = useContext(SomeContext);

簡単に訳すと、props を利用することなく異なる階層のコンポーネントとデータの共有を行うことができます。

Contextとは?

Reactコンポーネントのツリーに対して「グローバル」とみなすデータについて利用するように設計されています。
コンポーネントの再利用をより難しくする為、慎重に利用しなくてはなりません。

Contextによってコンポーネントツリー間におけるデータの橋渡しについて、すべての階層ごとに渡す必要性がなくなり、propsバケツリレーをしなくても下の階層でContextに収容されているデータにアクセスできるようになりました。

useContextとは?

useContextとは、Context機能をよりシンプルに使えるようになった機能です。
親からPropsで渡されていないのに、Contextに収容されているデータへよりシンプルにアクセスできるというものです。

4階層のコンポーネント

一番上の親コンポーネントである App.js のファイルの中身は下記のようになります。ComponentABC については components ディレクトリを作成してその下に作成していきます。

App.js
import { createContext } from 'react';
import ComponentA from './components/ComponentA';

export const UserCount = createContext();

function App() {
  return (
    <div style={{ textAlign: 'center' }}>
      <h1>Learn useContext</h1>
      <UserCount.Provider value={100}>
        <ComponentA />
      </UserCount.Provider>
    </div>
  );
}

export default App;
components/ComponentA.js
import ComponentB from './ComponentB';

const ComponentA = () => {
  return (
    <div>
      <p>Componet A</p>
      <ComponentB />
    </div>
  );
};

export default ComponentA;
components/ComponentB.js
import ComponentC from './ComponentC';

const ComponentB = () => {
  return (
    <div>
      <p>Componet B</p>
      <ComponentC />
    </div>
  );
};

export default ComponentB;
components/ComponentC.js
import { useContext } from 'react';
import { UserCount } from '../App';

const ComponentC = () => {
  const count = useContext(UserCount);
  return (
    <div>
      <p>Componet C</p>
      <p>{count}</p>
    </div>
  );
};

export default ComponentC;

Context 用のコンポーネントの作成方法

App.js ファイルの中で createContext を実行していましたがより汎用的にするために Context 用のコンポーネントの作成を行います。

CountContext.js ファイルの中にはコンポーネント間で共有したいデータ、関数を記述します。

context/CountContext.js
import { createContext, useState, useContext } from 'react';

const CountContext = createContext();

export function useCountContext() {
  return useContext(CountContext);
}

export function CountProvider({ children }) {
  const [count, setCount] = useState(100);

  const value = {
    count,
    setCount,
  };

  return (
    <CountContext.Provider value={value}>{children}</CountContext.Provider>
  );
}

App.js ファイルでは CountContext コンポーネントから CountProvider 関数を import します。

App.js
import React from 'react';
import './App.css';
import ComponentA from './components/ComponentA.js';
import { CountProvider } from './context/CountContext';

function App() {
  return (
    <div className="App">
      <h1>Learn useContext</h1>
      <CountProvider>
        <ComponentA />
      </CountProvider>
    </div>
  );
}

export default App;

ComponentC コンポーネントでは CountContext コンポーネントから useCountContext 関数を import します。useContextuseCountContext の中で使われているで ComponentCimport する必要はありません。

components/ComponentC.js
import React from 'react';
import { useCountContext } from '../context/CountContext';

const ComponentC = () => {
  const { count, setCount } = useCountContext();

  return (
    <div>
      <p>Componet C</p>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
};

export default ComponentC;

関数を追加方法

context/CountContext.js
import { createContext, useState, useContext } from 'react';

const CountContext = createContext();

export function useCountContext() {
  return useContext(CountContext);
}

export function CountProvider({ children }) {
  const [count, setCount] = useState(100);

  const countDown = () => {
    setCount(count - 1);
  };

  const value = {
    count,
    setCount,
    countDown,
  };

  return (
    <CountContext.Provider value={value}>{children}</CountContext.Provider>
  );
}

複数の Context の設定方法

Context APIContext は 1 つではなく複数設定することも可能です。方法は簡単で2 つの Provider コンポーネントでラップするだけです。

CountProviderAnotherCountProvider は逆でも構いません。

App.js
import './App.css';
import ComponentA from './components/ComponentA';
import { CountProvider } from './context/CountContext';
import { AnotherCountProvider } from './context/AnotherCountContext';

function App() {
  return (
    <div className="App">
      <h1>Learn useContext</h1>
      <CountProvider>
        <AnotherCountProvider>
          <ComponentA />
        </AnotherCountProvider>
      </CountProvider>
    </div>
  );
}

export default App;
10000leaves10000leaves

useReducer

状態管理のためのフックで、useState と似たような機能です。useStateuseReducerに内部実装されています。

(state, action) => newState という型のreducer を受け取り、現在のstatedispatch関数の両方を返します。

const [state, dispatch] = useReducer(reducer,'初期値')

reducerstateを更新するための関数で、dispatchは、reducerを実行するための呼び出し関数です。 (変数を宣言するときに、stateの更新方法をあらかじめ設定しておくことが出来ます。)

dispatch(action)で実行

  • actionは何をするのかを示すオブジェクト
  • {type: increment, payload: 0}のように、typeプロパティ(actionの識別子)と値のプロパティで構成されている。

useReducer()を使用したカウンター

//useReducerをimport
import React, {useReducer} from 'react'
import Button from '@mui/material/Button';
import ButtonGroup from '@mui/material/ButtonGroup';

//counterの初期値を0に設定
const initialState = 0
//reducer関数を作成
//countStateとactionを渡して、新しいcountStateを返すように実装する
const reducerFunc = (countState, action)=> {
//reducer関数にincrement、increment、reset処理を書く
//どの処理を渡すかはactionを渡すことによって判断する
  switch (action){
    case 'increment':
      return countState + 1
    case 'decrement':
      return countState - 1
    case 'reset':
      return initialState
    default:
      return countState
  }
}
const Counter = () => {
//作成したreducerFunc関数とcountStateをuseReducerに渡す
//useReducerはcountStateとdispatchをペアで返すので、それぞれを分割代入
  const [count, dispatch] = useReducer(reducerFunc, initialState)
//カウント数とそれぞれのactionを実行する<Button/>を設置する
  return (
    <>
      <h2>カウント:{count}</h2>
      <ButtonGroup color="primary" aria-label="outlined primary button group">
        <Button onClick={()=>dispatch('increment')}>increment</Button>
        <Button onClick={()=>dispatch('decrement')}>decrement</Button>
        <Button onClick={()=>dispatch('reset')}>reset</Button>
      </ButtonGroup>
    </>
  )
}

export default Counter
10000leaves10000leaves

useCallback

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

useEffectと同じように、依存配列(=[deps] コールバック関数が依存している要素が格納された配列)の要素のいずれかが変化した場合のみ、メモ化した値を再計算します。

const cachedFn = useCallback(fn, dependencies);

メモ化とは

メモ化とは同じ結果を返す処理について、初回のみ処理を実行記録しておき、値が必要となった2回目以降は、前回の処理結果を計算することなく呼び出し値を得られるようにすることです。

イベントハンドラーのようなcallback関数をメモ化し、不要に生成される関数インスタンスの作成を抑制、再描画を減らすことにより、都度計算しなくて良くなることからパフォーマンスを向上が期待できます。

基本

sampleFuncは、再レンダーされる度に新しく作られますが、a,bが変わらない限り、作り直す必要はありません。

const sampleFunc = () => {doSomething(a, b)}

usecallbackを使えば、依存配列の要素a,bのいずれかが変化した場合のみ、以前作ってメモ化したsampleFuncの値を再計算します。一方で全て前回と同じであれば、前回のsampleFuncを再利用します。

const sampleFunc = useCallback(
  () => {doSomething(a, b)}, [a, b]
);

メモ化されたコールバックからの state 更新

場合によっては、メモ化されたコールバックから前回の state に基づいて state を更新する必要があります。

この handleAddTodo 関数は、次の todo リストを計算するために todos を依存値として指定します。

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos([...todos, newTodo]);
  }, [todos]);
  // ...

通常、メモ化された関数からは可能な限り依存値を少なくしたいと思うでしょう。何らかの state を次の state を計算するためだけに読み込んでいる場合、代わりに更新用関数 (updater function) を渡すことでその依存値を削除できます。

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos(todos => [...todos, newTodo]);
  }, []); // ✅ No need for the todos dependency
  // ...

ここでは、todos を依存値として内部で読み込む代わりに、どのように state を更新するかについての指示(todos => [...todos, newTodo])を React に渡します。

エフェクトが頻繁に発火するのを防ぐ

エフェクトから呼び出す必要がある関数を useCallback でラップする

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const createOptions = useCallback(() => {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }, [roomId]); // ✅ Only changes when roomId changes

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // ✅ Only changes when createOptions changes
  // ...

関数型の依存値を必要としないようにする場合

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() { // ✅ No need for useCallback or function dependencies!
      return {
        serverUrl: 'https://localhost:1234',
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ Only changes when roomId changes
  // ...

カスタムフックの最適化

function useRouter() {
  const { dispatch } = useContext(RouterStateContext);

  const navigate = useCallback((url) => {
    dispatch({ type: 'navigate', url });
  }, [dispatch]);

  const goBack = useCallback(() => {
    dispatch({ type: 'back' });
  }, [dispatch]);

  return {
    navigate,
    goBack,
  };
}
10000leaves10000leaves

useMemo

useMemoは関数の結果を保持するためのフックで、何回やっても結果が同じ場合の値などを保存(メモ化)し、そこから値を再取得します。

不要な再計算をスキップすることから、パフォーマンスの向上が期待出来ます。
useCallbackは関数自体をメモ化しますが、useMemoは関数の結果を保持します。

const cachedValue = useMemo(calculateValue, dependencies)

メモ化とは

メモ化とは同じ結果を返す処理について、初回のみ処理を実行記録しておき、値が必要となった2回目以降は、前回の処理結果を計算することなく呼び出し値を得られるようにすることです。

都度計算しなくて良くなることからパフォーマンス向上が期待できます。

基本

依存配列が空の場合

const sampleMemoFunc = () => {
  const memoResult = useMemo(() => hogeMemoFunc(), [])

  return <div>{memoResult}</div>
}

依存配列=[deps] へ空配列を渡すと何にも依存しないので、1回のみ実行します。
つまり、依存関係が変わらない場合はキャッシュから値をとってきます。

依存配列に値が入っている場合

props.nameの値が変わったときだけ関数を再実行させたい場合は以下のように書きます。

const sampleMemoFunc = (props) => {
  const memoResult = useMemo(() => hogeMemoFunc(props.name), [props.name])

  return <div>{memoResult}</div>
}

依存配列=[deps] へ変数を並べると、変数のどれかの値が変わった時にfuncを再実行します。
つまり、依存関係が変わった場合に再実行します。

10000leaves10000leaves

useRef

useRef は、新しいレンダーをトリガしないようにしたい時に使用します。
また、useStateのようにコンポーネント内での値を保持することが出来ます。

基本

const refObject = useRef(initialValue)

//例
const number = useRef(100);
console.log(number.current); // 100

cuseRefは、.currentプロパティが渡された引数(初期値はinitialValue)をrefObjectへ返します。
この引数の値が書き換え可能な.currentプロパティーの値であり、.currentプロパティ内に保持することができます。

useRefとuseState

useRefを利用するとtextstate更新時にのみコンポーネントの再レンダリングが発生します。

const App = () => {
  const inputEl = useRef(null);
  const [text, setText] = useState("");
  const handleClick = () => {
    setText(inputEl.current.value);
  };
  console.log("レンダリング!!");
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={handleClick}>set text</button>
      <p>テキスト : {text}</p>
    </>
  );
};