Kohei Blog

夫・父親・医療系エンジニア

ReactHooksについてのまとめ

Hooksとは?

React16.8.0で追加された機能

  • クラスコンポーネントよりもコード量が少なくなる

  • ロジックを分離できるので、ロジックの再利用やテストがしやすい。

フック API リファレンス

主なフックとしてあげられているものをまとめてみる

useState

stateと、state更新関数を返すフック

このフックを利用すれば、コンポーネント内でstate管理ができる

useStateは戻り値として、state変数とstate更新関数をタプルとして返すので、分割代入で受け取る

import { useState } from "react";

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

  const onClickSetCount = () => {
    setCount(count + 1);
  };
  return (
    <div className="App">
      <p>{count}</p>
      <button onClick={onClickSetCount}>add</button>
    </div>
  );
}

ちなみに「state」とは、画面に表示されるデータやUIの状態など、アプリケーションが保持している情報(データや値)のこと

また、「state管理」とは、stateの保持とstateの更新をすることを指す

注意:レンダリングごとに state 変数は一定

const plusThreeDirectly = () =>
		[0, 1, 2].forEach((_) => setCount(count + 1));
// 1

const plusThreeWithFunction = () =>
    [0, 1, 2].forEach((_) => setCount((c) => c + 1));
// 3

この挙動の違い

state 変数はそのコンポーネントレンダリングごとで一定

plusThreeDirectly() はそのレンダリング時点での count が 0 だったら、それを 1 に上書きする処理を 3 回繰り返すことになる

state 変数を相対的に変更する処理を行うときは、

前の値を直接参照・変更するのは避け、必ず setCount((c) => c + 1) のように関数で書くこと。

クラスコンポーネントでは this.state には常に最新の値が入ってるのに対し、State Hook ではレンダリングごとに state 変数は一定というちがいがあるの

注意:呼び出しはその関数コンポーネントの論理階層のトップレベ

条件文や繰り返し処理の中で呼びだすのはタブー

const Counter: VFC<{ max: number }> = ({ max }) => {
  const [count, setCount] = useState(0);

  if (count >= max) {
    const [isExceeded, setIsExceeded] = useState(true);
    doSomething(...);
  }

TypeScriptでuseStateを使う際の注意

stateの型が推論できるかどうか

外部APIから値を取得し、Stateに入れる場合だと、初期値として渡せる型を持ったデータがない。

そういうときは型推論に任せず、useState に明示的に型引数を渡してあげる必要がある

const [author, setAuthor] = useState<User>();

Userオブジェクト型を型引数として渡し、useStateの引数には何も渡していない

→ author は User オブジェクトを格納でき、初期値が undefined の変数になる。

undefined ではなく、明示的に null を入れたい場合はこうする

useState<User | null>(null)

引数に [] だけをわたすとなんの配列かわからないので、 useState<Article[]>([]); とする

オブジェクト配列の場合(TypeScript)

import {useState } from "react";
interface Todo {
  id: number;
  title: string;
  complete: boolean;
}

const data = [
  {
    id: 1,
    title: "todo1",
    complete: true
  },
  {
    id: 2,
    title: "todo2",
    complete: false
  },
  {
    id: 3,
    title: "todo3",
    complete: false
  }
];

export default function Todos() {
  const [todos, setTodos] = useState<Array<Todo>>(data);

  // const [todos, setTodos] = useState<Todo[]>(data);

<Array<Todo>> は <Todo[]> と同義

[ { } ] の形をあらわす。

オブジェクトのプロパティ型を定義するには interface を使う

interface Todo {
  id: number;
  title: string;
  complete: boolean;
}

useEffect

副作用って?英語では side-effect

→ コンポーネントの状態を変化させて、それ以降の出力を変えてしまうこと

コンポーネントがレンダーされるたびに副作用を実行

useEffect(() => {
	console.log('component render')
})

コンポーネントがレンダーされたときに1度だけ実行

useEffect(() => {
	console.log('component render')
},[])

副作用に依存する配列が更新したときだけ実行

useEffect(() => {
	 console.log(count)
},[count])

副作用内で関数をreturnすると、その関数はコンポーネントがアンマウントもしくは副作用が再実行されたときに実行される。

ライフサイクルメソッドでいうとそれぞれ以下に対応している

  • useEffect の第2引数に空配列を渡す ... componentDidMount

  • 何も渡さない ... componentDidUpdate

useEffect完全ガイド

メモ化

関数コンポーネントの中に、計算リソースを多大に消費する処理が内包されていたとして、結果が同じなのにレンダリングのたびに再計算されるのを防ぐ

パフォーマンス最適化のために、必要なときだけ計算し、「メモ’

コンポーネントが再レンダリングされたら、関数も再定義されてしまう。

それを防ぐために useCallback と useMemoを使う

依存配列にpropsを指定すれば、props が変わったときだけ、関数の再定義が行われるようになり、不要な再レンダリングを防ぐことができる。

useCallback

関数定義そのものをメモ化する

const reset = useCallback(() => setTimeLeft(limit), [limit]);

useMemo

関数の実行結果をメモ化する

const primes = useMemo(() => getPrimes(limit), [limit]);

useRef

refオブジェクトを生成するHooks

ユースケース

  • 初回のレンダリング時にテキストフォームにフォーカス

  • onClickによってフォームの入力値を取得

import { useEffect, useState, useCallback, useRef } from "react";

const refInput = useRef();

useEffect(() => {
    console.log(refInput.current);
    // => <input value="refInput."></input>

    refInput.current.focus();
  }, []);

return (
	<input onChange={onChangeText} value={text} ref={refInput} />
)

あらゆる書き換え可能な値を保持しておくことができる

const timerId = useRef();

useEffect(()=>{
  timerId.current = setInterval(60,1000)

  return () => clearInterval(timerId.current)
},[])

useRef で最新のタイマーIDを保持するようにできる

useRefはuseStateと違い、値の変更がコンポーネントの再レンダリングを発生させないのがポイント

useReducer

複雑なState更新時のsetter関数として

**import React, { useState, useReducer } from 'react';

export default function App() {
  const data = {
    id: 1,
    name: 'hobehobe',
    phone: '03-0000-1111',
    email: 'mail@example.com',
    admin: false
  };
  // const [user, setUser] = useState(data);

  const [user, setUser] = useReducer(
    (user, newDetails) => ({ ...user, ...newDetails }), // dispatch
    data // initial state
  );

  const handleClick = () => {
    // useStateだと、スプレッド構文で展開する必要がある
    // setUser({ ...user, admin: true });
    setUser({ admin: true });

    /*
    変更部分のみ更新される
    admin: true
    email: "mail@example.com"
    id: 1
    name: "hobehobe"
    phone: "03-0000-1111"
    */
  };

  console.log(user);
  return (
    <>
      <button onClick={handleClick}>click</button>
    </>
  );
}**

useContext と組み合わせて、reduxっぽく使う

immer

useContext

useReducer

npm install use-immer immer
import React, { useEffect, useState, useReducer } from "react";
import { useImmerReducer } from "use-immer";

import StateContext from "./StateContext";
import DispatchContext from "./DispatchContext";

const App = () => {
// stateの初期値を定義
  const initialState = {
    loggedIn: Boolean(localStorage.getItem("complexappToken")),
    flashMessages: [],
    user: {
      // 初期値はローカルストレージから取得
      token: localStorage.getItem("complexappToken"),
      username: localStorage.getItem("complexappUsername"),
      avatar: localStorage.getItem("complexappAvatar"),
    },
  };

/*
  immerを使って、recuderを定義
  毎回オブジェクト全体をreducer で定義する必要がなくなる。
  変更部分のみ、処理するだけでいい
  */
  const ourReducer = (draft, action) => {
    switch (action.type) {
      case "login":
        draft.loggedIn = true;
        draft.user = action.data;
        return;
      case "logout":
        draft.loggedIn = false;
        return;

      case "flashMessage":
        draft.flashMessages.push(action.value);
        return;
    }
  };

// reducer と state初期値
  const [state, dispatch] = useImmerReducer(ourReducer, initialState);

return(
	<StateContext.Provider value={state}>
	<DispatchContext.Provider value={dispatch}>
	  <Component />
	</DispatchContext.Provider>
	</StateContext.Provider>
)

}

DicpatchContext.js


import { createContext } from "react";

const DispatchContext = createContext();

export default DispatchContext;

StateContext.js

import { createContext } from "react";

const StateContext = createContext();

export default StateContext;

コンポーネントでstateとdispatchにアクセスする

import React, { useContext } from "react";
import DispatchContext from "../DispatchContext";
import StateContext from "../StateContext";

const Component = () => {
const appDispatch = useContext(DispatchContext);
const appState = useContext(StateContext);

  const handleLogout = () => {
    appDispatch({ type: "logout" });
  };

}

素のアクションオブジェクトをdispatchしているが、

action creatorを作成したほうがよさそう

useContext

provider を作る

管理するStateごとに作る。

import { createContext, useState } from "react";

export const UserContext = createContext({});

export const UserProvider = (props) => {
  const { children } = props;

// stateを定義
  const [userInfo, setUserInfo] = useState(null);

// Providerのvalueに渡
  return (
    <UserContext.Provider value={{ userInfo, setUserInfo }}>
      {children}
    </UserContext.Provider>
  );
};

App.js でStateを使いたいタグをラップする (ここでは全体)

import React, { UserProvider } from "./providers/UserProvider";
import { Router } from "./router/Router";
import "./styles.css";

export default function App() {
  return (
    <UserProvider>
      <Router />
    </UserProvider>
  );
}

コンポーネント側で使用

import React, { useContext } from "react";
import { UserContext } from "../../../providers/UserProvider";

export const UserIconWithName = (props) => {

	const { userInfo } = useContext(UserContext);

	const isAdmin = userInfo ? userInfo.isAdmin : false;

	const { setUserInfo } = useContext(UserContext);

	const onClickAdmin = () => {
	    setUserInfo({ isAdmin: true });
	  };

レンダリングの注意点

1つのプロバイダの中で渡している値(ここでは userInfo, setUserInfo )が 使われているコンポーネントが再レンダリングされてしまうので注意が必要。

更新時に「どのコンポーネントが再レンダリングされるか?」を意識しながら開発する必要がある。

コンポーネントmemo 化 など、再レンダリングの最適化を行う必要がある

参考

カスタムフック

src/hooks/useAllUsers.ts

import axios from "axios";
import { useState } from "react";
import { userProfile } from "../types/userProfile";
import { User } from "../types/api/user";

// 全ユーザー一覧を取得するカスタムフック
export const useAllUsers = () => {
  const [userProfiles, setUserProfile] = useState<Array<userProfile>>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const getUsers = () => {
    setLoading(true);
    setError(false);

    axios
      .get<Array<User>>("<https://jsonplaceholder.typicode.com/users>")
      .then((res) => {
        const data = res.data.map((user) => ({
          id: user.id,
          name: `${user.name}(${user.username})`,
          email: user.email,
          address: `${user.address.city}${user.address.suite}${user.address.street}`
        }));
        setUserProfile(data);
      })
      .catch(() => {
        setError(true);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  return {
    getUsers,
    userProfiles,
    loading,
    error
  };
};

コンポーネント側から利用

App.tsx

import { useAllUsers } from "./hooks/useAllUsers";

const { getUsers, userProfiles, loading, error } = useAllUsers();

const onClickFetchUser = () => getUsers();

useStateなどと同じように使用するだけで、簡単に実行できる。

独自フックの作成

関連リンク

カスタムフックは再利用できるため公開されているものを利用することもできる

useHooks

react-use|GitHub

Collection of React Hooks