【React】useStateで値を非同期ではなく瞬時に更新する方法について

  • 2024-03-03
【React】useStateで値を非同期ではなく瞬時に更新する方法について

ReactのuseStateフックは、非同期的に状態を更新するため、値が瞬時に更新されない場合があります。

例えば以下のようなコード

import { useState } from 'react';

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

  function handleClick() {
    setCount(count + 1);
    console.log(count); // 0が出力される
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

このコードでは、useStateで初期値として0を渡しています。ボタンをクリックすると、countの値が1増加するように設定しています。しかし、console.log(count)を呼び出すと、ボタンをクリックしても0が出力されます。これは、useStateの値更新が非同期で行われるため、コンポーネントの再レンダリングが完了する前にconsole.log(count)が実行されているためです。

しかし、場合によっては関数コンポーネントにおいて保持したい値があってそれを同期的にすぐに更新したいということもあるでしょう。

useStateフックで値を瞬時に更新する方法はあるのか?

useStateフックで値を瞬時に更新する方法はありません。 useStateは非同期で値を更新するため、値が瞬時に更新されることはありません。

このため、同期的な値更新が必要な場合は、useState以外の手段を用いる必要があります。例えば、useRefフックを用いて値を更新することで、同期的に瞬時に値を更新することができます。useRefフックは、値を保持するために使用されますが、useStateと異なり、値の変更が非同期で行われることはありません。

useRefフックを用いて値を瞬時に更新する

useRefフックを使用して、瞬時に値を更新する例です。

import React, { useRef } from 'react';

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

  const handleClick = () => {
    countRef.current += 1;
    console.log(countRef.current);
  };

  return (
    <div>
      <p>{countRef.current}</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

export default Example;

この例では、useStateフックではなく、useRefフックを使用しています。countRef.currentを直接更新することで、値の変更が瞬時に行われます。ただし、値が更新されたことによる再レンダリングは行われないため、画面に反映されないことに注意してください。

ただし、useRefフックは、コンポーネントが再レンダリングされるときに値が保持されるため、コンポーネントのライフサイクルと合わせて使用する必要があります。また、Reactのイベントシステムとは別のものであるため、イベントリスナー内で使用する場合は、useCallbackフックを使用して、イベントハンドラーをメモ化する必要があります。

useRefをコンポーネントのライフサイクルと合わせて使用する

useRefフックがコンポーネントのライフサイクルに合わせて使用される例

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

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

  useEffect(() => {
    console.log('Component did mount');
    return () => {
      console.log('Component will unmount');
    };
  }, []);

  const handleClick = () => {
    countRef.current += 1;
    console.log(countRef.current);
  };

  return (
    <div>
      <p>{countRef.current}</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

export default Example;

useEffectフックを使用して、コンポーネントがマウントされたときとアンマウントされたときにログを出力しています。countRefは、useRefフックを使用して定義されており、クリックイベントが発生するたびに値がインクリメントされます。

useEffectフックの第2引数に[]を渡すことで、コンポーネントのマウント時に1回だけuseEffect内の処理が実行され、アンマウント時にはクリーンアップ処理が実行されます。

このように、useRefフックは、コンポーネントのライフサイクルに合わせて使用されることが望ましいです。それにより、コンポーネントの初期化時に値が初期化され、コンポーネントが破棄されるときに値が破棄されるため、メモリリークのリスクが低くなります。

useRef以外での代替手段は?

他にも以下のような代替手段があります。

  1. useReducerフックを使用する
  2. React Context APIを使用する
  3. Reduxを使用する

useReducerフックを使用する

useReducerフックは、複雑な状態を管理する場合や、複数の関連する状態を1つのオブジェクトで管理する場合に有用です。

注意: useReducerフックもuseStateと同様に非同期です。useReducerでstateを更新した場合でも、その更新はReactによってバッチ処理されるため、同期的には更新されません。

React Context APIを使用する

React Context APIは、コンポーネントツリー全体で共有する値を保持するために使用されます。これにより、propsを介して多くのコンポーネントに渡す必要がなくなり、コードがよりシンプルになります。

注意: React Context APIで提供される値の更新は、基本的には非同期的です。つまり、値が更新された際にすぐに反映されるわけではありません。

Reduxを使用する

Reduxは、複雑なアプリケーションで状態を管理するための予測可能な方法を提供するライブラリであり、複数のコンポーネントで共有される状態を効果的に管理することができます。ただし、Reduxを導入するには追加の学習コストがかかるため、小規模なアプリケーションでは使用しない方が良いでしょう。

Reduxは単一のストア(store)を持ち、そのストアの中に状態を保持しています。Reduxによる状態の更新は、同期的に行われます。ただし、Reactコンポーネント内でReduxストアの値を参照している場合、コンポーネントが再レンダリングされるタイミングはReactの更新処理に依存します。つまり、Reduxストアの値が変更されても、Reactコンポーネントが再レンダリングされるまで実際に変更が反映されない場合があります。


他の代替手段も紹介しましたが、結局useRefを使うのが無難ですね。

useRefの値を更新したときに再レンダリングする方法は?

useRefフックで値を更新しただけでは、関数コンポーネントは再レンダリングされません。

やはり関数コンポーネントにおいて値の更新による再レンダリングをするにはuseStateフックを組み込む必要があります。

以下は、useRefフックを使用して瞬時に値を更新し、コンポーネントを再レンダリングするサンプルコードです。useRefフックとuseEffectフックを使用して、再レンダリングする必要があるときにコンポーネントを再レンダリングすることができます。

import React, { useRef, useEffect, useState } from "react";

function Example() {
  const countRef = useRef(0);
  const [count, setCount] = useState(0);

  const updateCount = () => {
    countRef.current += 1;
    setCount(countRef.current);
  };

  useEffect(() => {
    console.log("Component rendered!");
  }, [count]);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={updateCount}>Update Count</button>
    </div>
  );
}

countRefという名前のrefを作成し、初期値として0を設定しています。updateCount関数は、countRefの値を1増やし、その値をsetCountを使用してcountステートに設定します。useEffectフックは、countが変更されたときにログを出力します。このように、useRefフックとuseEffectフックを組み合わせることで、refを使用してコンポーネントを再レンダリングできます。