React.js

Reactの基本Hooks入門:useState・useEffect・useRef・useContextの使い方

この記事でわかること

ReactのuseState・useEffect・useRef・useContextをTypeScriptの動く例で解説。状態更新、イベントの後始末、入力欄へのフォーカス、テーマ共有と使い分けを学びます。

Reactの基本機能を身につけるには、画面に使うデータ、外部との同期、DOMの操作、親から子への値の共有を分けて考えると整理しやすくなります。この記事では4つのHooksを、TypeScriptの小さな実装例で確認します。

4つのHooksの役割と準備

どの機能を使うか判断する

  • useState:入力値や選択状態など、変わったら画面に反映したい値を保持します。
  • useEffect:ブラウザーのイベントなど、Reactの外側の仕組みと同期します。
  • useRef:入力欄などのDOMへの参照や、表示の更新を伴わない値を保持します。
  • useContext:親が提供する値を、その配下のコンポーネントから読み取ります。

最初に必要になるのは、多くの場合useStateです。ほかのHooksは、具体的に何を実現したいかが決まってから選びます。

実行環境とHooksの呼び出しルール

React.jsの読書リスト入門で紹介したViteのReact・TypeScriptプロジェクトを利用できます。以下のコンポーネントをsrcディレクトリに作成し、最後のsrc/App.tsxでまとめて表示してください。例はReact 18でも使える記法です。

今回の4つのHooksは、関数コンポーネントかカスタムHookのトップレベルで呼び出します。条件分岐、繰り返し、イベントハンドラーの中では呼び出しません。条件によって処理を変える場合も、Hook自体の呼び出し順序は変えないようにします。

useState:画面に反映する値を管理する

入力欄とカウンターを実装する

src/StateExample.tsxを作成します。文字列と数値を別々のstateで保持し、入力欄の変更やボタンのクリックで更新します。

import { useState } from 'react';

export function StateExample() {
  const [name, setName] = useState('');
  const [count, setCount] = useState(0);

  function addThree() {
    setCount((current) => current + 1);
    setCount((current) => current + 1);
    setCount((current) => current + 1);
  }

  return (
    <section aria-labelledby="state-heading">
      <h2 id="state-heading">useState:入力とカウンター</h2>
      <label>
        名前
        <input value={name} onChange={(event) => setName(event.target.value)} />
      </label>
      <p>こんにちは、{name.trim() || 'ゲスト'}さん</p>
      <p role="status">カウント:{count}</p>
      <button type="button" onClick={() => setCount((current) => current + 1)}>
        1増やす
      </button>
      <button type="button" onClick={addThree}>3増やす</button>
      <button type="button" onClick={() => setCount(0)}>リセット</button>
    </section>
  );
}

更新関数と現在の値を区別する

setCountを呼んでも、そのイベント処理内のcountが直ちに書き換わるわけではありません。同じ処理内でsetCount(count + 1)を3回呼ぶと、いずれも同じレンダーの値を基に更新を要求するため、通常は1しか増えません。

この例では更新関数を3つ渡しています。各関数が順に更新途中の値を受け取るので、結果が3増えます。更新関数の中では通信やログ保存などをせず、受け取った値から次の値を計算します。

オブジェクトや配列はコピーして更新する

stateがオブジェクトの場合も、プロパティを直接代入して終わりにせず、新しいオブジェクトを更新関数へ返します。配列はmapfilterなどで新しい配列を作ります。具体的な配列更新は、前の記事の読書リストを参照してください。

入力された名前から挨拶文を作るような計算は、レンダー中に行えます。挨拶文まで別のstateにすると、名前との同期が余分に必要になります。

useEffect:外部イベントと同期する

オンライン状態を監視する

src/ConnectionExample.tsxを作成します。ブラウザーのオンライン状態を初回に読み取り、以降はイベントに応じて更新します。初期値をnullにすることで、サーバーで描画する場合もレンダー中にnavigatorを参照しません。

import { useEffect, useState } from 'react';

export function ConnectionExample() {
  const [online, setOnline] = useState<boolean | null>(null);

  useEffect(() => {
    function updateConnection() {
      setOnline(navigator.onLine);
    }

    updateConnection();
    window.addEventListener('online', updateConnection);
    window.addEventListener('offline', updateConnection);

    return () => {
      window.removeEventListener('online', updateConnection);
      window.removeEventListener('offline', updateConnection);
    };
  }, []);

  const label = online === null ? '確認中' : online ? 'オンライン' : 'オフライン';

  return (
    <section aria-labelledby="connection-heading">
      <h2 id="connection-heading">useEffect:ブラウザーの接続状態</h2>
      <p role="status">接続状態:{label}</p>
      <p>この表示だけでは、特定のAPIへの接続成功までは判断できません。</p>
    </section>
  );
}

クリーンアップで登録を解除する

Effectが返す関数は、コンポーネントを取り外すときなどに実行されるクリーンアップです。イベント登録と同じ関数を使って解除し、登録だけが残らないようにします。

依存配列が空なのは、このEffectがコンポーネント内の変化するpropsやstateを読み取っていないためです。依存値を使うEffectでは必要な値を配列に含めます。「実行回数を減らしたい」という理由で依存値を省略しないでください。

開発時の再実行と接続状態の限界

ルートでStrict Modeを有効にしている開発環境では、Effectのセットアップとクリーンアップが追加で実行されることがあります。登録と解除を対称に書き、何度実行されても登録が重複しない形にします。

navigator.onLineはブラウザーが判断する接続状態です。オンラインでも、認証切れやサーバー停止などでAPI通信は失敗します。この値だけで操作を禁止せず、実際の通信失敗は個別に扱います。開発者ツールのオフライン設定がこの値へ反映されるかはブラウザーにも依存します。

useRef:入力欄にフォーカスする

DOMへの参照を保持する

src/FocusExample.tsxを作成します。ボタンを押すと、参照している入力欄へフォーカスを移します。

import { useRef } from 'react';

export function FocusExample() {
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <section aria-labelledby="focus-heading">
      <h2 id="focus-heading">useRef:入力欄へ移動する</h2>
      <label>
        メモ
        <input ref={inputRef} type="text" />
      </label>
      <button type="button" onClick={() => inputRef.current?.focus()}>
        メモ欄にフォーカス
      </button>
    </section>
  );
}

stateとの違いを押さえる

inputRef.currentには、Reactが配置した入力欄のDOMが入ります。まだ配置されていない場合などはnullなので、オプショナルチェーンで確認してからfocus()を呼びます。

refに保持した値を変更しても、それ自体では再レンダーされません。クリック回数を画面に表示したいならuseState、DOMへの参照など表示を直接決めない情報にはuseRef、という基準で選べます。通常、refの読み書きはイベントやEffectで行い、レンダー中の処理へ持ち込まないようにします。

useContext:配下のコンポーネントへ値を共有する

テーマ設定をProviderから渡す

src/ThemeExample.tsxを作成します。親がテーマをstateとして保持し、Contextを通して子へ提供します。ProviderはReact 18でも使える.Provider記法にしています。

import { createContext, useContext, useState } from 'react';

type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');

function ThemePreview() {
  const theme = useContext(ThemeContext);
  return <p>子コンポーネントのテーマ:{theme === 'light' ? 'ライト' : 'ダーク'}</p>;
}

export function ThemeExample() {
  const [theme, setTheme] = useState<Theme>('light');

  return (
    <ThemeContext.Provider value={theme}>
      <section
        aria-labelledby="theme-heading"
        style={{
          background: theme === 'light' ? '#f1f7fa' : '#172b35',
          color: theme === 'light' ? '#172b35' : '#ffffff',
          padding: '1rem',
        }}
      >
        <h2 id="theme-heading">useContext:テーマを共有する</h2>
        <ThemePreview />
        <button
          type="button"
          aria-pressed={theme === 'dark'}
          onClick={() => setTheme((current) => current === 'light' ? 'dark' : 'light')}
        >
          ダークテーマ
        </button>
      </section>
    </ThemeContext.Provider>
  );
}

Contextは値の保持そのものを担当しない

テーマを保持しているのはuseStateです。Contextはその値をコンポーネントのツリーへ渡す経路です。子は、自分より上にある最も近い対応Providerの値を読み取ります。

対応するProviderが上にない場合は、createContextで指定した初期値が使われます。この初期値は、アプリのstateと自動的に同期する値ではありません。

propsと使い分ける

少数の親子間で値を渡すならpropsでも十分です。何段もの中間コンポーネントが値を受け渡すだけになったとき、Contextを検討すると意図がわかりやすくなります。共有値の変更は利用する子の再レンダーにつながるので、すべてのstateを一つのContextへ集める必要はありません。

4つの例をまとめて動作確認する

App.tsxで各コンポーネントを表示する

src/App.tsxを次の内容へ置き換えます。src/main.tsxからの呼び出しは、Viteのテンプレートのままで構いません。見た目はプロジェクトのCSSに従います。

import { StateExample } from './StateExample';
import { ConnectionExample } from './ConnectionExample';
import { FocusExample } from './FocusExample';
import { ThemeExample } from './ThemeExample';

export default function App() {
  return (
    <main>
      <h1>React Hooksの基本練習</h1>
      <StateExample />
      <ConnectionExample />
      <FocusExample />
      <ThemeExample />
    </main>
  );
}

確認する操作

  • 名前を入力すると挨拶が変わり、空白だけなら「ゲスト」になります。
  • カウントが0のとき「3増やす」を押すと3になり、リセットで0へ戻ります。
  • 接続状態はブラウザーの通知に応じて変わります。実際のAPI疎通確認とは区別します。
  • フォーカス用ボタンを押した直後に文字を入力すると、メモ欄へ入力されます。
  • テーマを切り替えると、背景色と子コンポーネントのテーマ名が変わります。

Viteプロジェクトでビルドを実行し、型エラーやimport漏れがないことも確認します。

npm run build

よくある疑問と次の学習

useMemoやuseCallbackも最初から必要ですか

今回の小さな例には必要ありません。これらは計算結果や関数の参照を保持するための機能です。まず正しく動く実装を作り、処理時間や不要な更新が問題になったときに、計測して導入を判断します。

Next.jsのApp Routerで使う場合はどうしますか

これらの対話的な例を使うクライアント側の入口ファイルには、importより前に'use client'を記述します。入口からimportされるすべてのファイルに同じ宣言を繰り返す必要はありません。今回のVite構成ではこの宣言は不要です。

基本機能を組み合わせて練習する

まずはカウンターの増減幅を変更する、入力欄を増やす、テーマを3種類にするなど、小さな変更を試してください。複数の本を扱う練習には読書リストの実装記事、型の理解にはTypeScriptの記事一覧が役立ちます。

公式ドキュメント

y.
WRITTEN BY

y_ymo10

SEの部屋で、JavaScript・TypeScript・React.js・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →