モダンReactの代表になっているhooksですが、使いこなせるととても便利です。
今まで、クラスコンポーネントや
SFC(Stateless Functional Component)として高階コンポーネントやレンダープロップス、Recomposeなどを使っていた方は使い勝手の良さに驚くでしょう。
また、これからReactを学習する人に向けてもhooksを使いこなすことはとても価値があり、
FC(Function Component)+hooksから入門するべきだと思います。
実際の現場でclassやSFCはレガシーであり、FC+hooksへの移行が進んでいるからです。
復習がてら、どんなhooksがあるのか、どんな時にどう使うのかをまとめました。
##前提として
React version 16.8.0 以降
hooksはFCでないと使用できない
import React, { useState, useEffect, useContext } from 'react'
const hooks = () => {
//関数コンポーネント内にhooksを書く
return null
}
読み込みが必要ですがここから先ははしょりますので、上記のように読込んでください。
またはReact.hooksの名前
でも可能です。
##コンポーネント内で状態管理をしたい
###useState
const hooks = () => {
const [count, setCount] = useState(0)
return(
<>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>ボタン</button>
</>
)
}
ステートフルな値と、それを変化させるための関数を分裂代入します。
上記は、ボタンを押すたびにsetCount
によって状態が変化して数字が増えていきます。
また、useState
の第一引数はinitialState(初期値)となっていて、ロジックが含まれている場合は、関数を渡すこともできます。
const [count, setCount] = useState(()=> 1*2*3)
####オブジェクトの更新の仕方に注意
クラスコンポーネントの setState メソッドとは異なり、下記では正常にstateが更新されません。
const hooks = () => {
const [user, setUser] = useState({ name: "test", age: "20" })
return (
<>
<div>{user.name}</div>
<button onClick={() =>
setUser({user.name = 'testNEW'})
}>
ボタン
</button>
</>
)
}
consoleでuser
を出すとtestNEW
が出力されてしまいます。
正常にname
の値のみ変更するためには、下記のようにスプレッド構文と併用してマージさせるように書きます。
const hooks = () => {
return (
<button onClick={() =>
setUser({...user, ...{ name: "testNEW"}})
}>
ボタン
</button>
)
}
##renderされた後に何か処理をしたい
###useEffect
いままでcomponentDidMount
をつかって、render後にAPIリクエストを行うといったアクションを取っていたと思いますが、useEffect
を使ってそれを実現できます。
正確に言うとcomponentDidMount
と componentDidUpdate
と componentWillUnmount
がまとまったものだと考えることができます。
const hooks = () => {
const [user, setUser] = useState({ name: "test", age: "20" })
//APIリクエストのサンプルです
const sampleGetRequest = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({ name: "NEW" })
}, 2000)
})
}
//render後に発火
useEffect(() => {
sampleGetRequest().then(response => {
setUser({ ...user, ...response })
})
}, [])
return <div>{user.name}</div>
}
上記のコードを実行すると、renderされてから2秒後に
useStateで持っている、userが更新され、viewが書き換わります。
####第二引数について
第二引数を引数なし、空の配列、値を入れた配列
で使い分けることによって、3種類の使い方ができます。
#####引数なし
useEffect(() => {})
コンポーネントがマウントされた後、更新された後に関わらず、毎回のレンダー時に処理を実行します。
componentDidMount と componentDidUpdateを併用しているのと似ています。
#####空の配列
useEffect(() => {}, [])
コンポーネントが初回にマウントされた後のみ処理を実行します。
componentDidMountを記述しているのと似ています。
#####値を入れた配列
const [name, setName] = useState('test')
const [age, setAge] = useState(25)
useEffect(() => {
console.log(`${name}は${age}才だ`)
}, [count])
配列内に入れた値を監視してくれます。
上記の例だと、配列の中に記入したcount
のデータの内容が書き換わった時に処理が実行されます。
なので、age
のデータ内容が更新されても何も起きず、count
が更新された時のみlogが出るようになります。
####無限ループに注意
引数を無し、配列に値を入れた引数の場合、無限ループになる可能性があります。
useEffectの説明の最初のコードの引数を無くしてみました。
これだと無限ループに陥って、処理を繰り返してしまいます。
レンダー => setUser({ ...user, ...response }) => レンダー => setUser({ ...user, ...response }) => レンダー => 続く...
const sampleGetRequest = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({ name: "NEW" })
}, 2000)
})
}
useEffect(() => {
sampleGetRequest().then(response => {
setUser({ ...user, ...response })
})
})
なぜならsetUser({ ...user, ...response })
によってuser
の参照は毎回切れてしまうからですね。
{ name: "NEW", age: "20" }
から{ name: "NEW", age: "20" }
に変更されても、値は変わってないように見えて、参照は変わっているので、仮想DOMの差分を検知して再レンダーしてuseEffect発火という流れができてしまいます。
空の配列を引数に指定すれば治ります。
useEffect内で状態を変化させる際は気をつけましょう。
もっとuseEffectについて詳し知りたい方はuseEffect完全ガイドをご覧ください。
##コンポーネントツリー内で、値を共有したい
###useContext
通常だとデータを親から子に渡す際、propsとしてバケツリレーで渡していく必要があります。
ですが、useContextを使えば、バケツリレーの必要がなく、一気に下層コンポーネントに値を渡すことができます。
多くの階層を経由していくつかの props を渡すことを避けたいときはうってつけです。
クラスコンポーネントでもコンテクストは使えましたが、それのhooks版という感じです。
下記は、APP.jsxからTitle.jsxまで一気に(Header.jsxを飛び越えて)値を渡している例です。
import React, { createContext } from "react"
import Header from "./Header"
//最上部のコンポーネントではクラスコンポーネントと同じように、
//値を共有するためのコンテクストをcreateContextで作成
export const PageMetaContext = createContext("");
const App = () => {
const PageMeta = {
title: "タイトル",
description: "詳細です"
};
return(
//Header以下のツリーに共有できるようにプロバイダを使用、
//valueに値を設定することで、ツリー内のどの子コンポーネントにも渡せる(今回は PageMeta)
<PageMetaContext.Provider value={PageMeta}>
<Header />
</PageMetaContext.Provider>
)
}
export default App;
値を使用する必要のないコンポーネントは何も特別なことはしなくてもいい
import React from "react"
import Title from "./Title"
const Header = () => (
<header>
<Title />
</header>
)
export default Header;
親のプロバイダで設定された値を使いたいコンポーネントのみuseContext
を使う
注意すべき点は、useContext
に渡す引数はコンテクストオブジェクト自体であること
import React, { useContext } from "react";
//ツリーの親で作成されたコンテキストを読み込む
import { PageMetaContext } from "./App";
const Title = () => {
//ここでuseContextを使う
const { title, description } = useContext(PageMetaContext);
return (
<>
<h1>{title}</h1>
<span>{description}</span>
</>
)
}
export default Title;
これで親で設定したPageMeta
の中の文字列がviewに表示されます。
####useContext に渡す引数に注意
useContext
に渡す引数はコンテクストオブジェクト自体でなければいけません。
上記の例だと、titleのみ使いたいからといって、
const title = useContext(PageMetaContext.title);
にするとエラーが起きてしまうので気をつけましょう。
##複数の複雑なstateを1つにして、templateをスッキリさせたい
###useReducer
useReducerはuseStateと同じく、コンポーネント内で状態管理をするためのhookで、
useStateの状態管理をより堅牢であり、複雑なロジックが絡んだステートを更新するのに適しています。
Reduxに馴染みがあれば簡単ですが、初見だとわかりにくく使用するのにも気がひけるので順をおって説明します。
下記のようなテキストボックスに入力するした値をリストにできるサンプルをつくりました。
2つのコンポーネントによって構成され、
下記はuseRecucerをもち、子コンポーネントからの入力によってリストをレンダーさせる親側のコンポーネントです。
import React, { useReducer } from "react"
import InputArea from "./InputArea"
const initialState = { input: "", items: [] }
const reducer = (state, action) => {
switch (action.type) {
case "updateInput":
return { ...state, input: action.payload };
case "resetInput":
return { ...state, input: "" };
case "addItem":
return { ...state, items: [...state.items, action.payload] };
case "removeItem":
const filteredItems = state.items.filter(v => v.key !== action.payload);
return {
...state,
items: filteredItems
};
case "resetItems":
return initialState;
default:
throw new Error();
}
}
const Lists = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<ul>
{state.items.map((item, index) => (
<li key={index}>
{item.title}
<button
onClick={() =>
dispatch({ type: "removeItem", payload: item.key })
}
>
削除
</button>
</li>
))}
</ul>
<InputArea input={state.input} dispatch={dispatch} />
</div>
)
}
export default Lists
まずuseReducerの宣言部分ですが、
const [state, dispatch] = useReducer(reducer, initialState);
-
state
:ステートフルな値 -
dispatch
:値を更新したいという旨をreducerに通知するための関数 - useReducerの第1引数の
reducer
:値を更新するためのロジックが書いてある関数 - useReducerの第2引数の
initialState
:値の初期値、関数でもOK。
実際にレンダリングのために扱う値は、state
の中身になります。
値の更新は、reducer
と action
の2つの重要な役割によって行われます。
####reducer
const reducer = (state, action) => {
switch (action.type) {
case "updateInput":
return { ...state, input: action.payload };
case "resetInput":
return { ...state, input: "" };
case "addItem":
return { ...state, items: [...state.items, action.payload] };
case "removeItem":
const filteredItems = state.items.filter(v => v.key !== action.payload);
return {
...state,
items: filteredItems
};
case "resetItems":
return initialState;
default:
throw new Error();
}
};
reducer関数は、actionのtypeによって行う処理を分岐させるため、switch文で書きます。
引数の中身は
-
state
:現在のステート -
action
:typeとpayloadが入ったオブジェクト- type:
"updateInput"
のような処理をするための名前 - pyload:action関数から渡させる任意の値
- type:
今回のreducerの中には下記のしたい処理が書いてあります。
- inputの値を更新
- inputの値を空にする
- 配列にinputの値を追加
- 配列から、keyが同じオブジェクトを削除
- inputと配列の内容を初期化
- typeがない時エラーを投げる
それぞれのtypeごとに実行される関数から返される値は、新しいステートになります。
それにより、新しいレンダーが行われます。
そして何を元にtypeがわかり、処理が振分けされるのかというと、action
によって行われます。
####action
dispatch({ type: "removeItem", payload: item.key })
actionは簡単にいうと、reducerへの更新依頼です。
useReducerで宣言時に代入されたdispatch
を使うことで実現します。
{ type: "removeItem", payload: item.key }
はreducer内のactionで使うことができます。
####下位コンポーネントでstateの更新をさせる
下記は先ほどのTodo.jsxの子コンポーネントで、テキストボックスから値を入力し、stateを更新する役割を持っているコンポーネントです。
import React from "react";
const InputArea = ({ input, dispatch }) => {
const hundleChange = event => {
dispatch({ type: "updateInput", payload: event.currentTarget.value });
};
const addItem = () => {
dispatch({
type: "addItem",
payload: { title: input, key: new Date().getTime() + Math.random() }
});
dispatch({ type: "resetInput" });
};
const resetItems = () => {
dispatch({ type: "resetItems" });
};
return (
<div>
<input type="text" onChange={e => hundleChange(e)} value={input} />
<button onClick={addItem}>追加</button>
<button onClick={resetItems}>リセット</button>
</div>
);
};
export default InputArea;
もしuseState
を使っている場合は親コンポーネントからコールバック関数を受け取り、発火させることでstateの更新ができましたが、
useReducer
を使っている場合は、dispatch
を渡し、子コンポーネント内でstate変更のactionを記述すれば良いので、stete更新のためのロジックが親コンポーネントに溜まらないので、見通しがよくなりますし、コンポーネントの役割が明確になります。
また、コンポーネントツリーが大きくなっている場合は、propsでなく、useContext
と組み合わせて使えば、小規模アプリならreduxを使わずに、useContext
+useReducer
でコードの肥大化を防ぐことができるでしょう。
##レンダーごとに計算を実行されないように処理をキャッシュしたい
###useMomo
useMemoはメモ化された値を返します。
メモ化というのはプログラムの高速化のための最適化する技術の1つで、処理結果を保持しておき、あるトリガーがあるまで処理を行わずに保持してある値を返すことを言います。
下記の例はuseMome
を使っていないコンポーネントで、
- テキストボックスの値を入力することで、stateの
input
が更新 - ボタンを押すと、stateの
count
の三乗が計算され、consoleが出る
というものです
import React, { useState, useMemo } from "react";
const Memo = () => {
const [count, setCount] = useState(2);
const [input, setInput] = useState("");
const newCount = () => {
console.log("計算します");
return Math.pow(count, 3);
};
return (
<div>
<input
type="text"
value={input}
onChange={e => setInput(e.target.value)}
/>
<p>{newCount()}</p>
<button onClick={() => setCount(prev => prev + 1)}>increment</button>
</div>
);
};
export default Memo;
上記だとonChangeが発火されるたびに、consoleに計算します
が出力されます。
これは計算結果が変わることはないのにnewCount
関数が実行され、無駄な計算を繰り返していることになります。
今回の例は、そこまで複雑な計算ではないですが、もっと複雑になったり、他の処理も重なってくるとレンダリング速度のパフォーマンスの低下に繋がります。
ここでuseMome
を使います
const newCount = useMemo(() => {
console.log("render");
return Math.pow(count, 3);
}, [count]);
これによりnewCount
はメモ化された値の入る変数になり、引数として配列に入っているcount
に変更があるまで処理は実行されません。
先ほどの問題は解消され、onChange
が発火して、stateのinput
が書き換わっても、再度計算されなくなりました。
####useMemoの第二引数の指定に注意
- 引数なし:全てのstate, propに依存します。結果的に不必要に処理が行われるためメモ化する意味がなくなります。
- 空配列[]:何にも依存しません。処理が行われるのはレンダー直後のみになります。
- 値を入れた配列:配列内の変数に変更があるたびに、処理が実行されます。複数入力可能です。useMemoを使う場合は、忘れないようにしましょう。
##親からpropsとして渡される関数による無駄なレンダーを避けたい
###useCallback
簡単にいうとuseMome
の関数版です。
useCallback
の場合はメモ化された関数を返します。