なにそれ? React Hooks
はじめに
React や Next.js といったフロントエンドのお勉強をしており、以前以下の記事を公開しました。
しかし、React Hooks については軽く触れた程度だったので、改めてこちらでまとめてみました。
React Hooks とは
React v16.8 から React Hooks という機能が登場しました。
一体何者なのでしょうか。まずは公式ドキュメントを確認します。
React では、useState やその他の use で始まる関数はフック (Hook) と呼ばれます。
フックは、React がレンダーされている間のみ利用可能な特別な関数です。
フックを使うことで、さまざまな React の機能に「接続 (hook into)」して使用することができます。
初めてこのドキュメントを見たときの直感は「ちょっとよく分からない」でした。
要約すると、React には様々な機能があり、フック(use
から始まる関数)を使うことで利用できるということです。
では、2文目の「レンダーされている間のみ利用可能」とはどういうことなのでしょうか。
これは React のレンダープロセスを理解する必要があります。
React での画面更新は以下の 3 つのステップで行われます。
- レンダーのトリガ(お客様の注文を厨房に伝える)
- コンポーネントへのレンダー(厨房で注文の品を料理する)
- DOM へのコミット(テーブルに注文の品を提供する)
React はレンダーステップでコンポーネントが決まったら、差異があるかを確認します。
これまでの DOM と差異があった場合のみ、DOM を変更してコミットします。
コミットされた DOM はコンポーネントにマウントされ、コンポーネントのライフサイクルがスタートします。
コンポーネントのライフサイクル
React コンポーネントは マウント → 更新 → アンマウント というライフサイクルを持ちます。
以下公式ドキュメントの引用です。
すべての React コンポーネントは同じライフサイクルを持ちます。
画面に追加されたとき、コンポーネントはマウントされます。
(大抵はインタラクションに応じて)新しい props や state を受け取ったとき、コンポーネントは更新されます。
画面から削除されたとき、コンポーネントはアンマウントされます。
つまり「レンダーされている間」とは、レンダーがトリガされてからコミットされるまでの間ということになります!
ここでは、主要な組込みフック(React で事前に定義済みのフック)について紹介します。
useState
公式ドキュメント[1]ではこのように説明されています。
useState は、コンポーネントに state 変数 を追加するための React フックです。
コンポーネント内で動的に変わる値を保持し、その値が変わるとコンポーネントが再レンダリングされます。
useState
の基本構文は以下の通りです。
const [count, setCount] = useState(0);
// count:現在の状態の値
// setCount:状態を更新するための関数
// useState(0):初期値として0を設定
useState
左辺の変数は慣習として、 [something, setSomething]
のように命名します。
state とは
コンポーネントによっては、ユーザ操作によって画面の表示内容を変更する必要があります。
例えば、以下のようなものが例として挙げられます。
- フォーム上でタイプすると入力欄が更新される
- 画像カルーセルで「次」をクリックすると表示される画像が変わる
- 「購入」をクリックすると買い物かごに商品が入る
コンポーネントは、現在の入力値、現在の画像、ショッピングカートの状態といったものを「覚えておく」必要があります。
React では、このようなコンポーネント固有のメモリのことを state と呼びます。
初期値
useState
の引数で state の初期値として値もしくは関数を指定できます。
関数を指定するときには、関数の実行結果ではなく関数そのものを渡すべきです。
例えば、以下の場合は createInitialTodos
という初期化関数の実行結果を渡しています。
const [todos, setTodos] = useState(createInitialTodos());
この場合、createInitialTodos()
の値は初回レンダーのみ利用されますが、レンダーごとにcreateInitialTodos()
が実行されてしまいます。
不要な関数を無駄に実行していることになり、パフォーマンスに影響します。
この事象を避けるためには以下のように初期化関数そのものを渡します。
const [todos, setTodos] = useState(createInitialTodos);
set 関数
useState
は state を更新するための関数として set 関数を返します。
set 関数には戻り値がなく、引数は値か、更新関数を渡します。
set 関数による state 更新は React によりキューイングされます。
イベントハンドラ内のすべてのコードが実行されるまで、React は state の更新処理を待機します。
少し分かりづらいのでレストランを例に理解を深めましょう。
レストランで店員がお客さんの注文を取る状況を想像してください。
店員は最初の料理の注文を聞いた瞬間にキッチンにかけこむわけではありません。
お客さんの注文を最後まで聞き、訂正がある場合はそれも聞き取り、さらには他のお客さんからの注文もまとめて受け取るはずです。
React も同様で、state 更新(注文)がトリガーされてすぐ更新処理をするわけではなく、
必要な更新処理を取りまとめてからレンダリング(キッチンへの伝達)をします。
useEffect
公式ドキュメント[2]ではこのように説明されています。
useEffect は、コンポーネントを外部システムと同期させるための React フックです。
外部システムと同期というのは、データ取得、DOM の操作、タイマーの設定といった副作用を伴う処理となります。
useEffect
を使うことで副作用を伴った処理をレンダリング後に呼び出すことができます。
useEffect
の基本構文は以下の通りです。関数コンポーネントのトップレベルで宣言します。
useEffect(() => {
副作用関数 // 第一引数:副作用関数(例: データ取得)
}, [依存配列]); // 第二引数:副作用関数の実行タイミングを制御する依存配列
第一引数には関数が、第二引数には配列が入ります。
第二引数で設定した依存値が更新された場合に、第一引数で指定した副作用関数が実行されます。
なお、第二引数は省略可です。省略した場合はレンダリングされる度に副作用関数が実行されます。
しかし、無限ループを避けるため、実際には省略することはほぼありません。
初回レンダリング時のみ副作用関数を実行
副作用関数を初回レンダリング時のみ実行させたい場合は、第二引数に空配列[]を指定します。
useEffect(() => {
副作用関数
},[]) // 空配列[]を指定
依存配列の値が変化したとき副作用関数を実行
依存配列の値が変化したときに副作用関数を実行させたい場合は、依存する変数を第二引数の配列に指定します。
const [count, setCount] = useState(0)
useEffect(() => {
console.log(count)
},[count]) // count 変数を依存配列に指定
クリーンアップ
副作用の再実行時やアンマウント時に、前回実行した副作用を削除したい場合があります。
例えば、チャットルームへ接続する副作用関数を持ち、ルーム ID に依存する Effect があるとします。
初回接続時・接続先変更時・アンマウント時の挙動をを考えてみます。
タイミング | 期待する動作 |
---|---|
初回接続時 | エフェクトが A ルームに接続する |
接続先変更時 | エフェクトが A ルームを切断し、B ルームに接続する |
アンマウント | エフェクトが B ルームを切断する |
ここでいうルームの切断がクリーンアップとなります。
useEffect
では第一引数で関数を return することでクリーンアップが実行できます。
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect(); // roomId で指定されたルームに接続する
return () => {
connection.disconnect(); // クリーンアップ関数として切断処理を実装
};
}, [roomId]); // roomId を依存配列に指定
useRef
公式ドキュメント[3]ではこのように説明されています。
useRef は、レンダー時には不要な値を参照するための React フックです。
useRef
では任意の型のデータを保持することができ、React によって再レンダー間で保持されます。
state とは違い、値を変更してもコンポーネントは再レンダリングをトリガーしません。
useRef
の基本構文は以下の通りです。引数に任意の型の初期値を渡すことができます。
let ref = useRef(0) // 初期値として 0 を設定
ref は以下のような current
という単一のプロパティを持つ JavaScript オブジェクトを返します。
{
current: 0
}
保持した値はref.current
で取得することができます。
また、この値はミュータブル、つまり読み書きが可能となっています。
ref.current = ref.current + 1 // ref の値を +1 する
ベストプラクティス
useRef
のベストプラクティスとして以下の 2 点があります。
useRef
は例外的に利用する
useRef
が適しているのは、外部システムやブラウザ API と連携する場合など一部のユースケースのみとなります。
useState
と比較して useRef
は制約が少ないため有用に思えますが、ほとんどのケースでは useState
の利用が推奨されます。
レンダー中に ref.current
を読み書きしない
React は、コンポーネント本体の関数が純関数のように振る舞うことを期待しています。
純関数とは
純関数には以下の特徴があります。
- 入力値(props、state、context)が同じなら、常に同じ出力(JSX)を返す。
- 呼び出し順が変わったり、引数を変えて呼び出されたりしても、他の呼び出し結果に影響を与えない。
レンダー中に ref
を読み書きすると、これらに違反してしまいます。
ref
の読み書きはイベントハンドラやエフェクトからのみ行うようにする必要があります。
例えば、以下は NG ケースです。
function MyComponent() {
myRef.current = 123 // 🚩 レンダリング中に書き込んでいる
return <h1>{myOtherRef.current}</h1> // 🚩 レンダリング中に読み取っている
}
以下は利用してもよい OK ケースです。
function MyComponent() {
useEffect(() => {
myRef.current = 123 // ✅ Effect 内のため書き込みしてもよい
})
function handleClick() {
doSomething(myOtherRef.current) // ✅ イベントハンドラ内のため読み取りしてもよい
}
}
ref と state の違い
ref と state の比較は以下の通りです。
ref | state |
---|---|
useRef(initialValue) は { current: initialValue } を返す |
useState(initialValue) は [value, setValue] を返す |
変更しても再レンダーがトリガされない | 変更すると再レンダーがトリガされる |
ミュータブル いつでも current の値を更新できる |
イミュータブルstate の更新には、set 関数を使用する |
レンダー中に current の値を読み書きするべきではない |
state の読み取りはいつでも可能だが、書き込みは各レンダーのタイミングでしか変更されない |
useContext
公式ドキュメント[4]ではこのように説明されています。
useContext はコンポーネントでコンテクスト (Context) の読み取りとサブスクライブ(subscribe, 変更の受け取り)を行うための React フックです。
props によるバケツリレーを行わずに、コンポーネント間でデータを共有するためのフックです。
親コンポーネントで定義した「コンテクスト」を、子コンポーネントがネストを気にせずに直接利用できます。
通常、上位のコンポーネントで SomeContext.Provider
を使用してコンテクストの値を指定し、
下位のコンポーネントで useContext(SomeContext)
を呼び出してコンテクストを読み取ります。
コンテクストの値の指定
まずは、コンポーネント外部で createContext 関数 によってコンテクストを作成します。
import { createContext } from 'react'
const SomeContext = createContext('first')
createContext
の引数はコンテクストのデフォルト値となります。デフォルト値は不変です。
createContext
の戻り値はコンテクストオブジェクトを返します。
コンテクストオブジェクト.Provider
で内部のコンポーネントに対してコンテクストの値を指定できます。
指定はvalue
という props のみ有効となります。
例えば以下の場合は、Page(とその内部の)コンポーネントは、渡されたコンテクストの値を参照できます。
function App() {
const [something, setSomething] = useState('second')
return (
<SomeContext.Provider value={something}>
<Page /> // Page コンポーネント配下のコンテクストは 'second' となる
</SomeContext.Provider>
)
}
コンテクストの値の取得
設定されたコンテクストを取得するために利用するのが useContext
です。
useContext
の基本構文は以下の通りです。
const value = useContext(SomeContext);
引数は createContext
で作成したコンテクストオブジェクトが入ります。
戻り値は呼び出したコンポーネントに対応するコンテクストの値を返します。
useContext
を呼び出したコンポーネントの上位かつ最も近い SomeContext.Provider
に渡された value が取得されます。
Provider が設定されていない場合は、createContext
の引数で設定したデフォルト値になります。
function Page() {
const value = useContext(SomeContext)
return (
<div>
{value} // 'second'が表示される
</div>
)
}
コンテクストの値の更新
コンテクストを更新するには、state
と組み合わせます。
親コンポーネントで state を定義し、state をコンテクストプロバイダに渡します。
React は、コンテクストに変更があると、それを読み取っているコンポーネントを自動的に再レンダーします。
function MyPage() {
const [theme, setTheme] = useState('dark') // state を定義
return (
<ThemeContext.Provider value={theme}> // 定義した state を Provider に指定
<Form /> // コンテクストが変更されたら再レンダー
<Button onClick={() => {
setTheme('light') // ボタンをクリックしたら state を変更
}}>
Switch to light theme
</Button>
</ThemeContext.Provider>
)
}
useMemo
公式ドキュメント[5]ではこのように説明されています。
useMemo は、レンダー間で計算結果をキャッシュするための React フックです。
useMemo
はパフォーマンス改善を目的として使われます。
逆に言えば、それ以外の用途では使うべきではないということになります。
useMemo
の Memo はメモ化(Memoization)のことです。
メモ化
純関数は入力値が同じなら、常に同じ出力を返すものです。
過去の純関数の呼び出し結果を保存し、同じ入力がきたときにキャッシュ値を返すことをメモ化といいます。
React では明示的にメモ化しない場合、関数オブジェクトはレンダーごとに生成されます。
つまり、レンダーされたときに入力値が同じであっても関数は実行され、その実行結果を使います。
それを避けるために用いるのが、useMemo
となります。
構文
useMemo
の基本構文は以下の通りです。
const memoizedValue = useMemo(
() => 純関数(純関数の引数), // 第一引数:純関数
[依存値] // 第二引数:依存配列
)
第一引数はキャッシュしたい値を導出する純関数です。
第二引数は第一引数で指定した関数内で参照されている値の配列です。
戻り値は関数の実行結果が返されます。
React は初回レンダー中に第一引数の関数を呼び出します。
次回以降のレンダーでは、直前のレンダーと依存値が変化していなければ、同じ値を再度返します。
依存値が変化していれば、第一引数の関数を呼び出して結果を返し、結果をメモ化します。
ユースケース
useMemo
を利用するのに適したユースケースとしては、負荷が高い計算処理を実行する場合などです。
とは言っても、負荷が高い計算処理という抽象的な考え方では使うべきか迷うことがあります。
React の公式ドキュメントで一定の指標を挙げています。
以下のように計算処理を計測できます。
実行後に filter array: 0.15ms
といったログがコンソールに表示されます。
console.time('filter array')
const memoizedValue = calculateSomething(value)
console.timeEnd('filter array')
この計算結果が 1 ms 以上であればメモ化は有効と考えることができます。
また、以下のようにuseMemo
でラップすることで、処理時間がどれだけ減少したかを確認できます。
console.time('filter array')
const memoizedValue = useMemo(() => {
return calculateSomething(value)
}, [value])
console.timeEnd('filter array')
useCallback
公式ドキュメント[6]ではこのように説明されています。
useCallback は、再レンダー間で関数定義をキャッシュできるようにする React フックです。
React で関数をメモ化して、不要な再生成を防ぐためのフックです。
useCallback
の構文は以下の通りです。
const memoizedCallback = useCallback(() => {
// 関数の処理
}, [依存値]);
// 第1引数:メモ化したい関数
// 第2引数(依存配列):この配列内の値が変わったときだけ、新しい関数が生成されます。空配列[]の場合、最初のレンダリング時のみ関数が作られます。
useMemo
との違い
useMemo
では関数の実行結果をメモ化しますが、useCallback
では関数自体をメモ化します。
具体例として、props で関数を渡している場合を考えます。
以下は Form
コンポーネントに handleSubmit
という関数を渡しています。
<Form onSubmit={handleSubmit} />
Form
がメモ化されている場合、props に渡す値が変わっていない場合は Form の再レンダーをスキップしたいと考えるはずです。
props が毎回異なる値だと、Form
のメモ化は無意味になってしまうためです。
そこで、useMemo
を使って handleSubmit
をメモ化する場合、以下のようになります。
export default function Page({ productId, referrer }) {
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
})
}
}, [productId, referrer])
return <Form onSubmit={handleSubmit} />
}
可読性が低いコードになってしまいました。
useMemo
の場合は useMemo(() => {
の中に別の純関数を返す必要があるためです。
このような余計な入れ子を避けるフックが useCallback
です。
上記の例は useCallback
を使って以下のように記述できます。
export default function Page({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
})
}, [productId, referrer])
return <Form onSubmit={handleSubmit} />
}
これら 2 つのコードは完全に同じ挙動をします。
useCallback
のメリットは、余計な関数の入れ子が不要になることだけです。
それ以外の違いは何もありません。
ユースケース
useCallback
のユースケースは基本的には以下の 3 つしかありません。
メモ化されたコンポーネントに props で渡す関数
上記の例で挙げたものです。
関数はたとえ同じ処理でもレンダーのたびに別の関数オブジェクトが作成されてしまいます。
props で渡す先がメモ化されている場合は、useCallback
による関数のメモ化が有効となります。
useEffect
や useMemo
の依存に含まれている関数
原理は同じで依存に関数が含まれる場合もレンダーごとに毎回実行されてしまいます。
そのため、依存配列に関数を含める場合は、useCallback
でのメモ化が有効となります。
ただし、この場合は以下のように関数依存を排除して useCallback
を取り除ける可能性があります。
function ChatRoom({ roomId }) {
- const createOptions = useCallback(() => { // useCallback を削除
- return {
- serverUrl: 'https://localhost:1234',
- roomId: roomId
- }
- }, [roomId])
useEffect(() => {
+ function createOptions() {
+ return {
+ serverUrl: 'https://localhost:1234',
+ roomId: roomId
+ };
+ }
const options = createOptions()
const connection = createConnection(options)
connection.connect()
return () => connection.disconnect()
- }, [createOptions]) // 関数を依存配列から削除
+ }, [roomId])
}
カスタムフックが返す関数
React は独自のフックとしてカスタムフックを定義することができます。
カスタムフックとして分離することは、責任の分離をしたことになります。
カスタムフック内の処理や仕様は他のコンポーネントから影響されるべきではありません。
ここでいう仕様というのはメモ化も例外ではありません。
例えば、関数を返すカスタムフックがあり、そのカスタムフックを呼び出す側の都合でメモ化が要件になったとします。
その場合、カスタムフック内部の処理を修正して、 useCallback
を使って return する関数をメモ化します。
これは、他のコンポーネントから影響を受けてしまったことになります。
そこで、機械的にカスタムフックで返す関数は全て useCallback
でメモ化すると、
カスタムフック実装者がメモ化が要件になっているのかを確認する必要や、
呼び出す側が関数がメモ化されているかどうかを意識する必要がなくなります。
カスタムフックで関数を返す場合は機械的に useCallback
を使いましょう!
と、いかにもチーム開発経験が豊富そうなことを言いましたが、私はほぼ皆無です。。
以下の記事を参考にさせていただきました。
おわりに
React Hooks について学習するにあたり様々なサイトを参考にさせていただきましたが、
やはり公式ドキュメントはまず読むべきだと思います。
日本語対応しており、例や図も豊富なため、初学者でも理解しやすいです。
Discussion