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がオブジェクトの場合も、プロパティを直接代入して終わりにせず、新しいオブジェクトを更新関数へ返します。配列はmapやfilterなどで新しい配列を作ります。具体的な配列更新は、前の記事の読書リストを参照してください。
入力された名前から挨拶文を作るような計算は、レンダー中に行えます。挨拶文まで別の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の記事一覧が役立ちます。