Hook(フック)

目次

1. Hookとは
2. useState
3. useEffect
4. useContext
5. useRef
6. useReducer
7. useMemo
8. useCallback
9. カスタムフック

1. Hookとは

Reactでは、Hooks apiと呼ばれる機能が用意されており、useStateやuseMemoなど、すべてuseから始まる関数名で定義されています。
hookは自身で作成することができ、それをcustom hook(カスタムフック)といいます。
カスタムフックは名前がuseから始まる関数で、Reactの提供する他のhookと同じルールが課されます。

2. useState

useStateは、状態変化をUIに反映させるための一時保存領域を確保するための機能です。
コンポーネントにはそれぞれデータ(状態)を持たすことができ、その状態の変更でUIが変更されたことを判断します。

import {useState} from "react";  //扱うには、インポートが必要

const [state, setState] = useState<stateの型>(初期値);

引数に初期値をとり、配列内で状態値state と 状態を更新するための関数setStateとなります。

2-1. setStateについて
stateの更新は更新関数setStateによってのみ行われ、以下のように引数の値で更新を行います。

setState(stateを更新する値);

また、stateの値に基づいて値を関数のように更新できます。

setState((state) => nextState(state))

2-2. useStateのルール

useStateを使うには2つのルールを守る必要があります。

  • コンポーネントの関数または、後述のcustom hookの内側から呼び出す
  • 関数のトップレベルから呼び出す
const DemoComponent = () => {
  const [state, setState] = useState<number>(4);
  if (state % 2 === 0) {
    return console.log('偶数');
  }
  return console.log('奇数');
};

そのため、以下の各例では許容されません。

  • ファイルのトップレベルから呼び出す
  • 通常の関数から呼び出す
  • 分岐内から呼び出す
  • 分岐後に呼び出す
  • ループ中で呼び出す

ファイルのトップレベルから呼び出す

const [state, setState] = useState<string>('初期値');

const DemoComponent = () => {
  return <></>;
};

通常の関数から呼び出す

const DemoComponent = () => {
  const [state, setState] = useState<string>('初期値');

  return {
    state,
    setState,
  };
};

分岐内から呼び出す

const DemoComponent = () => {
  if (loading) {
    const [state, setState] = useState<string>('初期値');
  }

  return <></>;
};

分岐後に呼び出す

const DemoComponent = () => {
  if (loading) {
    return <>Loading</>  
  }
  const [state, setState] = useState<string>('初期値');

  return <></>;
};

ループ中で呼び出す

const DemoComponent = () => {
  for (let i = 0; i < 5; i++) {
    const [state, setState] = useState<string>('初期値');
  }

  return <></>;
};

2-3. 例文

2-3-1. クリック分を1加算するカウント

App.jsx

import { useState } from "react";  //useStateを使用できるようにインポート

export default function Home() {
    const [counter, setCounter] = useState(0);  //1. useStateを定義  初期値を0

    const handleClick = () => {  //2-2. onClick={handleClick} で呼び出される
        setCounter(counter + 1);  //3. setCounter関数により、stateのcounterを+1
    };

    return (
        <div>
            <h1>useState</h1>
            <h1>{counter}</h1>  //0. 4. 現状のstateを取得し表示
            <button onClick={handleClick}>ボタン</button>  //2-1. ボタンをクリックすると、handleClick関数を呼び出し
        </div>
    )
}

2-3-1. クリック分を1加算するカウント(カスタムフック)

useHookDemo.jsx(ロジックのみ記述)

import { useState } from "react";   //1. useStateを使用できるようにインポート

export const useHookDemo = () => {   //1-1. カスタムフックを関数としてまとめ、他で使用できるようにexportを付与
    const [counter, setCounter] = useState(0);   //1-2. useStateを定義 初期値を0

    const handleClick = () =>   //2-1. onClick={handleClick} で呼び出される
        setCounter((counter) => counter + 1);   //2-2. setCounter関数により、stateのcounterを+1

    return [counter, {setCounter, handleClick}];   //1-3. 2-3. 処理結果を返す
};

App.jsx(ビューのみを記述)

import { useHookDemo } from "./useHookDemo";   //useHook.jsxのuseHook関数をインポート

const Home = () => {   //画面表示の関数
    const [counter, {handleClick}] = useHookDemo();   //1. カスタムフックを呼び出す

    return (
          <div>
          <h1>useState</h1>
          <h1>{counter}</h1>   //0. 現状のstateを取得し表示
          <button onClick={handleClick}>ボタン</button>   //2. ボタンをクリックすると、handleClick関数を呼び出し
          </div>
    )
}

export default Home;   //画面表示の関数にdefaultを付与

3. useEffect

useEffectは、リロードされたタイミングなのかなどのイベントで発火のタイミングを決めることができる。
第1引数にコールバック関数第2引数に発火のタイミングを指定。
配列を指定することで、ページがマウント(リロード)された時に発火する。

 useEffect(( ) => { }, [ ])

以下のように、配列の中にcountを指定すると、このcountが変更されたタイミングで発火される。

import {useState, useEffect} from "react";
import "./App.css";

const App = () => {
  //useState
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  }

  //useEffect
  useEffect(() => {
    console.log('Hello')  //ブラウザのコンソールに発火を確認できる
    //setCount(count + 1);  //useEffect内で依存関係のあるset関数を指定すると無限ループになるため、指定しない
  }, [count])

  return (
    <div className="App">
      <h1>useState, useEffect</h1>
      <button onClick={handleClick}>+</button>
      <p>{count}</p>
    </div>
  );
};

export default App;
import { useEffect, useState, useContext, useRef, useReducer, useMemo, useCallback } from "react";
import './App.css'
import TestInfoContext from "./main";
// import SomeChild from "./SomeChild";
import useLocalStorage from "./useLocalStorage";

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return state + 1;
    case "decrement":
      return state - 1;
    default:
      return state;
  }
};

function App() {
  const [count, setCount] = useState(0);
  const info = useContext(TestInfoContext);
  const ref = useRef();
  const [state, dispatch] = useReducer(reducer, 0);

  const handleClick = () => {
    setCount(count + 1);
  };

  //トリガーのタイミングを決める第二引数にタイミング
  //空の配列でページがマウントされたタイミングで発火
  //set関数を含めると無限ループとなってしまう
  useEffect(() => {
    console.log("Hello");
  }, [count])

  //useRef
  const handleRef = () => {
    console.log(ref);
    console.log(ref.current.value);
  };

  //useMemo
  //ブラウザのメモリに値を保存できる→簡単に値を取得できるようにする
  //ページ更新が遅ければ(重い処理)、useMemoを使用する
  //最初からuseMemoを使用することはなく、パフォーマンスが落ちてきたら使用する
  const [count01, setCount01] = useState(0);
  const [count02, setCount02] = useState(0);

  //このuseMemoのカウント2のみ重い処理のコード
  const square = useMemo(() => {
    let i = 0;
    while (i < 2000000000) {
      i++;
    }
    return count02 * count02;
  }, [count02]); //カウント2を指定

  //このuseMemoはカウント1とカウント2の両方が重い処理のコード
  // const square = () => {
  //   let i = 0;
  //   while(i < 2000000000) {
  //     i++;
  //   }
  //   return count02 * count02;
  // }

  //useCallBack????
  //ブラウザのメモリに関数を保存できる→簡単に値を取得できるようにする
  // const [counter, setcounter] = useState(0);

  // const showCount = useCallback(() => {
  //   alert('これは重い処理です');
  // }, [counter]);

  // const showCount = () => {
  //   alert('これは重い処理です');
  // }


  //カスタムフック
  const [age, setAge] = useLocalStorage("age", 24);




  return (
    <div className="App">
      <h1>useState, useEffect</h1>
      <button onClick={handleClick}>+</button>
      <p>{count}</p>

      <hr />
      <h1>useContext</h1>
      <p>{info.name}</p>
      <p>{info.age}</p>

      <hr />
      <h1>useRef</h1>
      <input type="text" ref={ref} />
      <button onClick={handleRef}>UseRef</button>

      <hr />
      <h1>useReducer</h1>
      <p>カウント:{state}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>

      <hr />
      <h1>useMemo</h1>
      <div>カウント1:{count01}</div>
      <div>カウント2:{count02}</div>
      <div>結果:{square}</div>
      <button onClick={() => setCount01(count01 + 1)}>+</button>
      <button onClick={() => setCount02(count02 + 1)}>+</button>

      <hr />
      <h1>useCallBack</h1>
      {/* <SomeChild showCount={showCount}/> */}

      <hr />
      <h1>カスタムフック</h1>
      <p>{age}</p>
      <button onClick={() => setAge(80)}>年齢をセット</button>

    </div>
  );
}

export default App;

4. useContext

コンポーネントの上下関係でpropsにより上位から下位へコンポーネントを順番にバケツリレーみたいに値を受け渡しを行なっていました。
useContextを使用すると、上位コンポーネントから使用したい下位コンポーネントへ直接値を渡すことができます。

4-1. 例文

main.jsx(Appコンポーネントへ値を渡す)

import React, {createContext}  from "react";
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

//グローバルで共有したい値
const userInfo = {
    name: 'nameTest',
    age: 10,
    gender: 'man',
}

//値を渡すための変数を用意
const UserInfoContext = createContext(userInfo);   //createContextでグローバルで扱うコンテキストを生成し、変数へ格納

ReactDOM.createRoot(document.getElementById('root')).render(
    <UserInfoContext.Provider value={userInfo}>  //Appコンポーネントで扱えるようにProviderを定義し、valueで値を指定
        <React.StrictMode>
            <App />   //このAppコンポーネントでUserInfoContextを扱いたい
        </React.StrictMode>
    </UserInfoContext.Provider>
)

export default UserInfoContext;   //どこでも扱えるようにexport

App.jsx

import { useContext } from "react";
import  UserInfoContext  from "./main";
import "./App.css";

const App = () => {
    const userInfo = useContext(UserInfoContext);

    return (
        <div className="App">
            <h1>useContext</h1>
            <p>{userInfo.name}</p>
            <p>{userInfo.age}</p>
            <p>{userInfo.gender}</p>
        </div>
    );
};

export default App;

5. useRef

useRefは、指定したHTMLの情報(文字数や高さなど)を参照する機能を持つ。

5-1. 例文

import { useRef } from "react";
import "./App.css";

const App = () => {
    const ref = useRef();  //フックスを用意

    const handleRef = () => {
        console.log(ref);  //クリック時に入力された情報を参照する
        console.log(ref.current.value);  //入力された文字列を参照できる 画像の場合aaaaa が出力される
    }

    return (
        <div className="App">
            <h1>useRef</h1>
            <input type="text" ref={ref}/>  //入力テキスト
            <button onClick={handleRef}>UseRef</button>
        </div>
    );
};

export default App;

6. useReducer

useReducerとは、現状の状態と新しく来た情報を比較しその差分を更新する機能を持ちます。

6-1. 例文

import { useReducer } from "react";
import "./App.css";

//選択されたボタンに応じて現状の値に対し加算または減算を行う関数
const reducer = (state, action) => {
  switch(action.type) {
    case "increment":   //「+」ボタンでstateを+1
      return state + 1;
    case "decrement":   //「-」ボタンでstateを-1
      return state - 1;
    default:
      return state;
  }
}

const App = () => {
  //stateは現状値 dispatchは通知を出す
  const [state, dispatch] = useReducer(reducer, 0);  //第2引数の0は初期値

  return (
    <div className="App">
      <h1>useReducer</h1>
      <p>カウント:{state}</p>
      <button onClick={() => dispatch({type: "increment"})}>+</button>
      <button onClick={() => dispatch({type: "decrement"})}>-</button>
    </div>
  );
};

export default App;

7. useMemo

useMemoは、ブラウザのメモリにを保存することで、Reactのパフォーマンスをチューニングするための機能を持つ。

まず、useMemoを使用しない場合の処理を体感します。

例として「カウント2」のボタンを押したことによる結果を取得したいです。そのため、「カウント1」に関しては結果は求めていないためこの処理はスムーズに行う必要があります。
しかし、重い処理の再現を行うための関数があり、「カウント1」および「カウント2」のボタンを押下することによって、共にwhile文が10億回回るため、両方の結果が反映されるまで遅いです。

import { useState } from "react";
import "./App.css";

const App = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  //重い処理の再現を行うための関数
  const square = () => {
    let i = 0;
    while ( i < 1000000000) {  //10億回
      i++;
    }
    return count2 * count2;
  };

  return (
    <div className="App">
      <h1>useMemo</h1>
      <div>カウント1結果:{count1}</div>
      <div>カウント2結果:{count2}</div>
      <div>square関数の結果:{square()}</div>  //square関数を呼び出すため、square()
      <button onClick={() => setCount1(count1 + 1)}>カウント1+</button>
      <button onClick={() => setCount2(count2 + 2)}>カウント2+</button>
    </div>
  );
};

export default App;

上記のコードのように、関係のない「カウント1」のパフォーマンスを向上するためにuseMemoを使用していきます。

import { useState, useMemo } from "react";
import "./App.css";

const App = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  //重い処理の再現を行うための関数
  const square = useMemo(() => {  //useMemoは( )でラッピングする必要がある
    let i = 0;
    while ( i < 1000000000) {  //10億回
      i++;
    }
    return count2 * count2;
  }, [count2]);  //第2引数に思い処理を指定

  return (
    <div className="App">
      <h1>useMemo</h1>
      <div>カウント1結果:{count1}</div>
      <div>カウント2結果:{count2}</div>
      <div>square関数の結果:{square}</div>  //useMemoの( )でラッピングしているためsquareの( )は不要
      <button onClick={() => setCount1(count1 + 1)}>カウント1+</button>
      <button onClick={() => setCount2(count2 + 2)}>カウント2+</button>
    </div>
  );
};

export default App;

このように「カウント1」のパフォーマンスが向上できました。

useMemoは、メモリに保存するため、全ての実装に使用してしまうと、逆にパフォーマンスが落ちてしまう(オーバーヘッド)。
useMemoを使用するタイミングとしては、開発中にページ読み込みなどで遅いと思ったタイミングで使用するため、最初からuseMemoを使用するという考えにはならない。

8. useCallback

useCallbackとは、ブラウザのメモリに関数自体を保存することで、Reactのパフォーマンスをチューニングするための機能を持つ。

??

9. カスタムフック

カスタムフックのメリットは、以下のとおりです。

  • コンポーネントの複雑化を防ぐ
  • 機能を再利用できる
  • 複数のhooksをまとめることができる
  • テストが容易

9-1. コンポーネントの複雑化を防ぐ

カスタムフック適用前(ロジックとビューが混ざっている)

import { useState } from "react";

export const App = () => {
    const [count, setCount] = useState(0);

    // カウントを1増やす
    const countUp = () => setCount((count) => count + 1);
    // カウントを1減らす
    const countDown = () => setCount((count) => count - 1);

    return (
      <div>
          <p>{count}</p>
          <button onClick={countUp}>+1</button>
          <button onClick={countDown}>-1</button>
      </div>
    );
};

カスタムフック適用後

useHooks.js(ロジック用)

import { useState } from "react";

export const useCounter = () => {
  const [count, setCount] = useState(0);

  // カウントを1増やす機能を再利用できる
  const countUp = () => setCount((count) => count + 1);

  // カウントを1減らす
  const countDown = () => setCount((count) => count - 1);

  return [count, { setCount, countUp, countDown }];
};

App.js(ビュー用)

import { useCounter } from "./conpornents/Title";

export const App = () => {
  const [count, { countUp, countDown }] = useCounter();

  return (
    <div>
      <p>{count}</p>
      <button onClick={countUp}>+1</button>
      <button onClick={countDown}>-1</button>
    </div>
  );
};

9-2. 機能を再利用できる

ビューとロジックに分離して管理できるため、ロジックだけを再利用することも可能です。
このように、カスタムフックを作成しておけば、そのコンポーネントに作成したカスタムフックをimportするだけで使えるため、同じ処理を書く必要がなく、ロジックの再利用性が向上します。

9-3. 複数のhooksをまとめることができる

APIなどから取得した複数のロジックをカスタムフックにまとめることができます。

9-4. テストが容易

機能を再利用できるということはそれ単体で動かせる状態ということであり、単体で動かせるということはテストが簡単になるということです。
コンポーネントのテストとロジックのテストを分けることができるのは、カスタムフックの大きなメリットだと思います。

9-5. カスタムフックのルール

カスタムフックには公式が提唱しているルールが存在します。

9-5-1. 命名規則

React はカスタムフックもルールを違反してるかどうかを自動でチェックしてくれます。
そのため、命名規則を守らなかったらカスタムフックかどうか判別できなくなり、自動チェックもできなくなります
カスタムフックの命名は必ずuseで始まるようにします。

9-5-2. 戻り値

カスタムフックの戻り値は、プログラマーが自由に設定できます。
基本的には、以下に分けられます。

  • 特定のuse〇〇の形式に合わせる
  • 全部返す


特定のuse〇〇の形式に合わせる

基本的なReact hooksには戻り値にルールがあり、カスタムフックもそのルールに合わせる方法があります。
メリットとしては、以下の2点です。

  • 戻り値だけで、どのようなフックであるかある程度予測がつく
  • カスタムフックの処理自体にも統制をとることができる
React hooks戻り値
useState配列([state, state更新関数])
useReducer配列([state, state更新関数])
useEffect戻り値なし
useRef1個
useContext1個

オブジェクトですべて返す

返したいものを全てオブジェクトに詰め込んで返します。
メリットとしては、以下の3点です。

  • 返却値を増やしたい時に変更がいらない
  • コンポーネントで呼び出す際に、返却値の命名が変更されることがない
  • テスト時に値を取りやすい (result.current.〇〇でとれる)

9-6. 例文

App.jsx

import "./App.css";
import useLocalStorage from "./useLocalStorage";

const App = () => {
  const [age, setAge] = useLocalStorage("age", 20);  //keyがage、valueが20

  return (
    <div className="App">
      <h1>カスタムフック</h1>
      <p>{age}</p>
      <button onClick={() => setAge(80)}>年齢をセット</button>  //ボタンをクリックすると、ストレージに80を更新
    </div>
  );
};

export default App;

useLocalStorage.jsx

import { useState, useEffect } from "react"

const useLocalStorage = (key, defaultValue) => {
    const [value, setValue] = useState(() => {
        const jsonValue = window.localStorage.getItem(key);  //ローカルストレージのgetItem(key)で取得
        if(jsonValue !== null) return JSON.parse(jsonValue);  //ローカルストレージ内にjson形式に変換

        return defaultValue;  //無ければ、defaultValueを返す
    });

    //このままではローカルストレージにkeyがある時だけ動作するため、Effectを使用する
    //発火のタイミングは、ボタンを押下時にストレージにセットする
    useEffect(() => {
        window.localStorage.setItem(key, JSON.stringify(value));
    }, [value, setValue])

    return [value, setValue];   //
};

export default useLocalStorage;

コメント