React Hooks 再入門
はじめに
過去にクラスコンポーネントでReactを書く機会が多く、関数コンポーネントのReact Hooks周りが複雑に感じていました。
関数コンポーネントの基礎を理解するためにReact Hooksを学び直します。
ちょうどReactのドキュメントが2023年3月にリニューアルされていたのでReact再入門。
開発環境
今回はNext.jsでプロジェクトを作成
npx create-next-app {projectname}
開発環境の構築方法として以下の方法が紹介されていました。
Start a New React Project
- Next.js
- Remix
- Gatsby
- Expo(for native apps)
- Next.js(App Router)
Reactの開発環境としてcreate-react-appは推奨されていないようです。
create-react-appが消えてしまったことの調査
ちなみにNext.jsのApp Routerは2023年5月5日にStableになったようです。
React Hooksとは
入力情報の記録や状態管理、親子間の変数の受け渡しなど複数のコンポーネントを組み合わせる際に必要な機能。
もともとクラスコンポーネントのstateで実現していた機能を関数コンポーネントでも実装できるようにした機能という理解です。
この記事でReactの歴史を知ることができます。
クラスコンポーネントから関数コンポーネントへの変遷についても理解しやすいです。
自分自身、2019年のHook API知識がキャッチアップできていなかったため今回の記事を書きました。
React今昔物語
Hooksの種類
大きく分けて7種類のHooksがあります。
ざっくり一言で説明すると
- State Hooks: ユーザーの入力情報を記録する
- Context Hooks: 親の変数を子コンポーネントで受け取る
- Ref Hooks: レンダリングに使用しない情報を記録
- Effect Hooks: 外部システムとの同期、イベントリスナ
- Performance Hooks: 再レンダリングのパフォーマンス最適化
- Other Hooks: ライブラリ開発時に利用されるHooks
- Your own Hooks: 独自定義によるカスタムフック
それぞれのHooksがどのように利用できるか実装例をまとめました。
State Hooks
useState
おそらく最も利用される機能でUIの入力値を変数に記録するために利用されます。
また値を変更するためのset関数も定義します。
この例ではテキストボックスの値が変更されるたびsetTextでtextを更新します。
import React, { useState } from 'react'
export const InputText = () => {
const [text, setText] = useState('hello')
function handleChange(e) {
setText(e.target.value)
}
return (
<div>
<input value={text} onChange={handleChange} />
<p>You typed: {text}</p>
</div>
)
}
Context Hook
useContext
親コンポーネントで定義した変数をcreateContext
を利用して子コンポーネントでも利用可能にします。
propsを受け渡すのではなく、親コンポーネントの変数が必要なコンポーネントのみuseContext
を利用して変数を受け取ることができます。
親から子コンポーネントに値を受け渡す方法として単純にpropsを渡すこともできますがその場合、バケツリレーのような形になりステートが変化した際に各コンポーネントが再レンダリングされパフォーマンスが低下します。またリファクタリングによる変数名の変更など管理上の問題を引き起こす原因になるため階層が深くなる場合propsで受け渡す実装は推奨されないようです。
Context Hookの説明はこの記事が非常にわかりやすいです。
この例ではcheck boxをクリックするとButtonコンポーネントのスタイルを変更します。
簡易的なdark/lightモードの切り替え実装の例です。
import styles from '@/styles/Home.module.css'
import { Button } from '@/components/button'
import { useState, createContext } from 'react'
export const ThemeContext = createContext(null)
export default function Home() {
const [theme, setTheme] = useState('light')
return (
<>
<main className={`${styles.main}`}>
<ThemeContext.Provider value={theme}>
<div>
<label>
<input
type="checkbox"
checked={theme === 'dark'}
onChange={(e) => {
setTheme(e.target.checked ? 'dark' : 'light')
}}
/>
Use dark mode
</label>
<div>
<Button>Click</Button>
</div>
</div>
</ThemeContext.Provider>
</main>
</>
)
}
import { useContext } from 'react'
import { ThemeContext } from '@/pages/index'
import styles from '@/styles/button.module.css'
export const Button = ({ children }) => {
const theme = useContext(ThemeContext)
const className = 'button-' + theme
return (
<button className={`${styles.button} ${styles[className]}`}>
{children}
</button>
)
}
.button {
border: 1px solid #777;
padding: 5px;
margin-right: 10px;
margin-top: 10px;
}
.button-light {
background: #fff;
color: #222;
}
.button-dark {
background: #222;
color: #fff;
}
Ref Hooks
useRef
レンダリングしない変数を扱うための機能。
ユーザーの操作回数やタイマーなどUIには表示せず内部的に変数に保持する際に利用します。
この例ではボタンが何回クリックされたかをrefで記録します。
stateでも実装できますが、refはUIとして入力を受け付ける必要はなく値を内部的に記録するだけなのでこの場合useRef
が適しています。
import { useRef } from 'react'
export const Counter = () => {
let ref = useRef(0)
function handleClick() {
ref.current = ref.current + 1
alert('You clicked ' + ref.current + ' times!')
}
return <button onClick={handleClick}>Click me!</button>
}
Effect Hooks
useEffect
APIなど外部システムに接続・同期するコンポーネントで利用されます。
外部システムと同期させないのであればuseEffect
を利用する必要はありません。
不用意にuseEffect
を利用しないためのベストプラクティスが紹介されています。
You Might Not Need an Effect
例えば、複数のstateを組み合わせて利用する際にuseEffect
を利用することは非推奨。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
}
useEffect
の使い所
外部のAPIサーバーとの通信が必要な際にuseEffect
を利用します。
この例では
①ChatRoomコンポーネントを表示
②propsのroomIdとserverUrlでコネクションを作成し接続 (setup code)
③serverUrlもしくはroomId(list of dependencies)が変更された場合、前のコネクションから切断される(cleanup code)
④変更されたserverUrlもしくはroomIdで②へ
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
// setup code
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect(); // cleanup code
};
}, [serverUrl, roomId]); // list of dependencies
// ...
}
useEffect
は他のHooksに比べ理解が難しい、、、
Performance Hooks
useMemo
再レンダリングする際に計算結果をキャッシュすることができる。
この例ではTodoListをフィルタする実装を想定しています。
再レンダリング時に計算処理を行うのではなくuseMemo
を利用することでキャッシュを利用してくれます。
①useMemoの一つ目の引数でフィルタ結果を返します(calculation function)
②フィルタなど①で必要な変数を配列で登録(list of dependencies)
③list of dependenciesとして登録された変数はレンダリング時に際比較され変更があれば再度①が実行されます。
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab), // calculation function
[todos, tab] // list of dependencies
);
// ...
}
まとめ
基本的なReact Hooksについてまとめましたが特にuseEffect
の使い所が難しい印象です。
公式のドキュメント(You Might Not Need an Effect)でもあったように使い方によってはコードの可読性が落ちデータの流れを追うのが難しくなります。
useEffect
は最終手段であり、useState
やuseMemo
など他のHooksで実装ができないかを検討する必要があります。
少し過激ですがこういった記事もあるようです。
最近話題となっているuseEffectの記事もとても参考になりました。
Discussion