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完全ガイド
メモ化
関数コンポーネントの中に、計算リソースを多大に消費する処理が内包されていたとして、結果が同じなのにレンダリングのたびに再計算されるのを防ぐ
パフォーマンス最適化のために、必要なときだけ計算し、「メモ’
コンポーネントが再レンダリングされたら、関数も再定義されてしまう。
それを防ぐために useCallback と useMemoを使う
依存配列にpropsを指定すれば、props が変わったときだけ、関数の再定義が行われるようになり、不要な再レンダリングを防ぐことができる。
useCallback
関数定義そのものをメモ化する
const reset = useCallback(() => setTimeLeft(limit), [limit]);
useMemo
関数の実行結果をメモ化する
const primes = useMemo(() => getPrimes(limit), [limit]);
useRef
refオブジェクトを生成するHooks
ユースケース
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