🦜

React Hooks 再入門

2023/05/06に公開

はじめに

過去にクラスコンポーネントでReactを書く機会が多く、関数コンポーネントのReact Hooks周りが複雑に感じていました。
関数コンポーネントの基礎を理解するためにReact Hooksを学び直します。

ちょうどReactのドキュメントが2023年3月にリニューアルされていたのでReact再入門。
https://react.dev/blog/2023/03/16/introducing-react-dev

開発環境

今回は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になったようです。
https://twitter.com/vercel/status/1654156302531063812

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を更新します。

components/inputText.tsx
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の説明はこの記事が非常にわかりやすいです。
https://blog.uhy.ooo/entry/2021-07-24/react-state-management/

この例ではcheck boxをクリックするとButtonコンポーネントのスタイルを変更します。
簡易的なdark/lightモードの切り替え実装の例です。

pages/index
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>
    </>
  )
}
components/button.tsx
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>
  )
}
styles/button.module.css
.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が適しています。

components/counter.tsx
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は最終手段であり、useStateuseMemoなど他のHooksで実装ができないかを検討する必要があります。

少し過激ですがこういった記事もあるようです。
https://dev.to/rem0nfawzi/dont-use-useeffect-3ca8

最近話題となっているuseEffectの記事もとても参考になりました。
https://zenn.dev/uhyo/articles/useeffect-taught-by-extremist

Discussion