ReactのuseState・useRef・constの違い:再レンダリングと値の保持を理解する

スポンサーリンク

ReactのuseState・useRef・constの違い:再レンダリングと値の保持を理解する

はじめに

Reactコンポーネントの中で値を扱うとき、useStateuseRefconst のどれを使えばいいか迷うことがあります。それぞれ「値の保持」と「再レンダリング」の動きが異なります。

値の保持 変更時に再レンダリング
const(通常変数) ✗(レンダリングごとに初期化)
useState
useRef

この違いを理解することで、どれを使うべきか判断できるようになります。


const(通常変数):レンダリングごとに初期化される

コンポーネント内で const で宣言した変数は、レンダリングのたびに初期値に戻ります。

function Counter() {
  let count = 0;  // レンダリングのたびに 0 に戻る

  const handleClick = () => {
    count += 1;
    console.log(count);  // クリックするたびに 1 が表示される(累積しない)
  };

  return <button onClick={handleClick}>{count}</button>;
}

count を増やしても画面は変わりません。変更が再レンダリングを引き起こさないため、ボタンのラベルは常に 0 のままです。

使いどころ:レンダリング内で一時的に使う計算値やスタイル定義など、保持する必要がない値に使います。

function UserCard({ user }) {
  const displayName = `${user.lastName} ${user.firstName}`;  // 毎回計算してよい
  const isAdmin = user.role === 'admin';

  return (
    <div>
      <p>{displayName}</p>
      {isAdmin && <span>管理者</span>}
    </div>
  );
}

useState:変更すると再レンダリングが走る

useState は値をレンダリング間で保持し、setState 関数を呼ぶと再レンダリングが走ります。UIに表示する値はこれを使います。

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);  // 呼ぶと再レンダリングが走り、画面が更新される
  };

  return <button onClick={handleClick}>{count}</button>;
}

直接変更しても再レンダリングは走らない

// NG:直接変更しても画面は変わらない
const handleClick = () => {
  count = count + 1;  // これは動かない
};

// OK:setState経由で変更する
const handleClick = () => {
  setCount(count + 1);
};

よく使うパターン

フォームの入力値

function Form() {
  const [name, setName] = useState('');

  return (
    <input
      value={name}
      onChange={e => setName(e.target.value)}
    />
  );
}

表示・非表示の切り替え

function Modal() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>開く</button>
      {isOpen && (
        <div>
          <p>モーダルの内容</p>
          <button onClick={() => setIsOpen(false)}>閉じる</button>
        </div>
      )}
    </>
  );
}

オブジェクトの状態管理

const [user, setUser] = useState({ name: '', email: '' });

// 一部だけ更新する場合はスプレッド構文で既存の値を保持する
setUser(prev => ({ ...prev, name: '田中太郎' }));

useRef:変更しても再レンダリングが走らない

useRef は値をレンダリング間で保持しますが、.current を変更しても再レンダリングは走りません。

import { useRef } from 'react';

function Counter() {
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current += 1;
    console.log(countRef.current);  // 値は累積するがUIは変わらない
  };

  return <button onClick={handleClick}>{countRef.current}</button>;
  // ボタンのラベルは常に 0 のまま(再レンダリングされないため)
}

よく使うパターン

DOMに直接アクセスする

useRef の最も典型的な使い方がDOMへのアクセスです。

import { useRef } from 'react';

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

  const handleFocus = () => {
    inputRef.current?.focus();  // 入力フィールドにフォーカスを当てる
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>フォーカス</button>
    </>
  );
}

タイマーIDの保持

import { useRef } from 'react';

function Timer() {
  const timerRef = useRef<number | null>(null);

  const start = () => {
    timerRef.current = window.setInterval(() => {
      console.log('tick');
    }, 1000);
  };

  const stop = () => {
    if (timerRef.current !== null) {
      clearInterval(timerRef.current);
    }
  };

  return (
    <>
      <button onClick={start}>開始</button>
      <button onClick={stop}>停止</button>
    </>
  );
}

タイマーIDはUIに表示しないので useRef が適切です。useState にすると更新のたびに不要な再レンダリングが発生します。

前回の値を記録する

import { useState, useRef, useEffect } from 'react';

function PreviousValue() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(0);

  useEffect(() => {
    prevCountRef.current = count;
  });

  return (
    <div>
      <p>現在: {count}</p>
      <p>前回: {prevCountRef.current}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

3つを比較する

実際に同じカウンターで動きの違いを確認します。

import { useState, useRef } from 'react';

function CompareAll() {
  let constCount = 0;                    // レンダリングごとに 0 に戻る
  const [stateCount, setStateCount] = useState(0);  // 変更で再レンダリング
  const refCount = useRef(0);            // 変更しても再レンダリングしない

  return (
    <div>
      <div>
        <p>const: {constCount}</p>
        <button onClick={() => { constCount += 1; console.log(constCount); }}>
          +1(画面は変わらない)
        </button>
      </div>
      <div>
        <p>useState: {stateCount}</p>
        <button onClick={() => setStateCount(stateCount + 1)}>
          +1(画面が更新される)
        </button>
      </div>
      <div>
        <p>useRef: {refCount.current}</p>
        <button onClick={() => { refCount.current += 1; console.log(refCount.current); }}>
          +1(値は増えるが画面は変わらない)
        </button>
      </div>
    </div>
  );
}

使い分けの判断フロー

値をUIに表示する?
├── YES → useState
└── NO → 値をレンダリング間で保持する必要がある?
          ├── YES → useRef(タイマーID、DOM参照、前回値など)
          └── NO  → const(計算値、スタイル、一時変数など)

まとめ

const useState useRef
値の保持
変更で再レンダリング
主な用途 一時的な計算値 UIに表示する値 DOM参照・タイマーID・前回値
  • UIに表示する値useState
  • UIに表示しないが保持したい値useRef
  • 保持する必要がない一時的な値const

APIフェッチやタイマーなど副作用の扱い方は「ReactのuseEffect入門:基本から気をつけることまで」を参照してください。