【React】useRefの応用、ひとつ前の状態と現在の状態を比較する方法(usePrevious)

2023年10月19日React

Reactで状態管理をしていると、「ひとつ前の状態」と「現在の状態」を比較する条件分岐したい場合などが多々あります。
そんなときに使えるのが、useRefを使ったusePreviousです。

useRefについて知りたい場合は、【React】でDOMを直接操作するuseRefの使い方とrefとstateの違いを参考にしてみてください。

ひとつ前の状態と現在の状態を比較する方法(usePrevious)

まず、usePreviousというカスタムフックを作成します。

import { useEffect, useRef } from 'react';

// カスタムフック usePrevious を作成
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

export default usePrevious;

usePreviousはカスタムフックで、useRefを使っています。
ref.currectの値を格納して戻り値として返しています。

usePreviousを使用してひとつ前の状態と現在の状態を比較する

import React, { useEffect, useState } from 'react';
import useCatalogData from './helpers/useCatalogData';
import usePrevious from './helpers/usePrevious';


// サイドバーのコンポーネント
const Sidebar = ({ cartItems, onRemoveFromCart, show, toggleMenu }) => {
  const catalogData = useCatalogData();

  // 最新のアイテムを取得する関数
  const getLatestItem = latestItemId => {
    let latestItem = null;
    for (const category of catalogData) {
      for (const item of category.items) {
        if (item.id === latestItemId) {
          latestItem = item;
          break;
        }
      }
      if (latestItem) {
        break;
      }
    }
    return latestItem;
  };

  // cartItems の最新のアイテムIDを取得
  const latestItemId =
    cartItems.length > 0 ? cartItems[cartItems.length - 1] : null;
  // 最新のアイテムを取得
  const [latestItem, setLatestItem] = useState(
    latestItemId ? getLatestItem(latestItemId) : null
  );

  // cartItems の変更を監視し、変更があった場合に latestItem を更新
  const prevCartItemsLength = usePrevious(cartItems.length); // 前回の cartItems の長さを記録

  const [isCartItemsIncreased, setisCartItemsIncreased] = useState(false);

  // cartItems の変更を監視し、変更があった場合に latestItem を更新
  useEffect(() => {
    if (cartItems.length === 0) {
      setLatestItem(null); // カートが空の場合、最新アイテムを null に設定
    } else {
      const latestItemId = cartItems[cartItems.length - 1];
      const updatedLatestItem = getLatestItem(latestItemId);
      setLatestItem(updatedLatestItem);
    }

    // 現在の cartItems の長さと前回の cartItems の長さを比較
    if (cartItems.length > prevCartItemsLength) {
      // cartItems が増加した場合の処理
      // console.log('Cart items increased');
      setisCartItemsIncreased(true);
    // } else if (cartItems.length < prevCartItemsLength) {
    } else  {
      // cartItems が減少した or 同じ場合の処理
      // console.log('Cart items decreased or are same');
      setisCartItemsIncreased(false);
    }

    // showでチェックリストがオープンになったときに増えているか確認
  }, [cartItems, show]);


  return (
    <aside className={`sidebar ${show ? 'show' : ''}`} id="sidebar">
      <div className="sidebar__close-area u-pc-none" onClick={toggleMenu}>
          {cartItems.length !== 0 && (
            <a className="sidebar__button" href="/contact/catalog/">
              <span className="-text">ダウンロード</span>
              <div
                className="icon-catalog-download"
                dangerouslySetInnerHTML={{ __html: IconCatalogDownload }}
              />
            </a>
          )}
        </nav>
      </div>
    </aside>
  );
};

export default Sidebar;

usePreviousの値をprevCartItemsLengthという変数に格納しています。
さらに、カート内が増えたかどうかを判定するための変数isCartItemsIncreasedをuseStateを使って用意しています。

あとは、if (cartItems.length > prevCartItemsLength)のようにして比較することで条件分岐することができます。

大事なポイントとして、useEffect内で使用します。
そして、上のケースでは、cartItemsが更新されるたびに再レンダリングされるように、useEffectの第二引数にcartItemsを含めます。

React v19での変更点とusePreviousの注意点

React v19では、以下のような変更や改善が行われていますが、この記事で紹介している usePrevious カスタムフックについても留意すべき点があります。

React v19の主な変更点

Strict Modeの動作改善

React v19では、Strict Modeでの動作がさらに強化されました。特に開発時のデバッグや問題検出に役立つ追加チェックが導入されています。useEffectの実行タイミングや回数が変わる可能性があるため、開発時にカスタムフックの挙動を確認する必要があります。

useEffectやuseRefの挙動に影響を与える潜在的変更

React v19では、React内部のレンダリング戦略が最適化されています。これにより、useEffectが予期しないタイミングで呼ばれる可能性がありますが、この記事の usePrevious のコードでは適切に動作します。これを確認するには、Strict Modeでテストすることを推奨します。

usePreviousをReact v19で使用する際のポイント

React v19でも、この記事で紹介したusePreviousはそのまま問題なく使用できます。ただし、以下の点を注意してください。

useRefの特性を理解する

useRefは、再レンダリング間で状態を保持するために使用されます。React v19でもこの動作は変更されていませんが、状態の同期が複雑になるケースではuseMemoやuseCallbackと組み合わせて使用することも検討してください。

Strict Modeでのデバッグ

React v19のStrict Modeでは、useEffectが2回実行される場合があります(開発モードのみ)。これが原因でusePreviousの値が意図せず更新されることがありますが、本番モードでは発生しません。それでも問題がある場合は、useEffectのデペンデンシーを見直すことで解決できる可能性があります。
例: React v19のStrict Modeを考慮したコード
以下は、React v19のStrict Modeでの動作を確認するためのコード例です。

import { useEffect, useRef } from 'react';

// usePrevious カスタムフック
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]); // value を依存配列に含めることで、正確な更新を保証
  return ref.current;
}

export default usePrevious;

React v19では、useEffect内の依存配列が正確に管理されていることを確認してください。また、Strict Modeで動作確認を行うことで、React v19における潜在的なバグや予期しない挙動を早期に発見できます。

React

Posted by devsakaso