👁️

VueのWatchをReactのカスタムフックで再現してみた

2023/10/06に公開

こんにちは! 株式会社 CastingONEの岡本です!

はじめに

弊社は現在、Vue から React への移行作業を進めています。この過程で、Vue で頻繁に利用していたウォッチャーのような機能が React にはデフォルトで存在しないことに気づきました。しかし、React の useRefuseEffect を駆使することで、Vue の Watch に似た動作を実現することができます。今回は、その方法を実装方法をまとめます。

成果物

今回実装したコードを先に codesandbox で共有しておきます。

実装方法

ここからは具体的な実装方法を書いていきます。

Vue の Watch

Vue の Watchの主な特徴として、以下が挙げられると思います。

  • 監視対象の新しい値と、前の値をコールバックで受け取ることができる
  • オブジェクトのようなネストが深い変更も検知できる
  • 即時実行か遅延して実行できるか選択できる
// 監視対象
const target = ref(0);

watch(
  target, // 監視対象
  (newValue, oldValue) => {
    // 変更後の新しい値と、変更前の値をコールバックで受け取れる
    console.log(`newValue: ${newValue}; oldValue: ${oldValue}`);
  },
  {
    immediate: true, // 即時ウォッチャーするかどうか
    deep: true, // ディープウォッチャーするかどうか
  }
);

第一引数に 監視する対象 を渡して、第二引数に 変更後の新しい値と変更前の値を受け取れるコールバック関数 を呼び出します。第三引数が オプションの設定 で、即時ウォッチャーやディープウォッチャーを使用するか設定することができます。

React のカスタムフックでの実装

上の Vue のウォッチャーの特徴を備えたカスタムフックを作っていこうと思います。

カスタムフックの I/F

カスタムフックのインターフェースは以下の通りです。

useWatchValue.tsx
/**
 * VueのWatcherのようなhooks
 * @param watchingState - 監視対象のvalue
 * @param callback - 新しいvalueと前のvalueを受け取るcb
 * @param option - option
 */
export const useWatchValue = <Type>(
  watchingState: Type,
  callback: (newValue: Type, prevValue: Type | undefined) => void,
  option: {
    immediate?: boolean
  }
) => {}

watchingState

  • 監視対象の値。型はジェネリクスで指定されたものが入る
  • この値の変更を監視し、変更があった場合にコールバックを実行する

callback

  • 新しい値(newValue)と前の値(prevValue)を受け取るコールバック関数
  • watchingStateが変更された場合に実行される関数

option

  • オプションを指定するオブジェクト
  • 指定できるオプションは即時ウォッチャー設定のimmediateのみとしています。

Vue の Watch に存在するdeepオプションについてですが、Vue はデータの変更がミュータブルに行われるため、ネストされたオブジェクトの変更を検出するためにdeepが必要ですが、React はそもそも参照自体を新しくする書き方をするため、このオプションを設定するためのインターフェースは提供していません。

実装

以下が、カスタムフックの実装内容です。

useWatchValue.tsx
/**
 * VueのWatcherのようなhooks
 * @param watchingState - 監視対象のvalue
 * @param callback - 新しいvalueと前のvalueを受け取るcb
 * @param option - option
 */
export const useWatchValue = <Type>(
  watchingState: Type,
  callback: (newValue: Type, prevValue: Type | undefined) => void,
  option: {
    immediate?: boolean;
  } = {}
) => {
  // 値の変更で再レンダリングを起こさないためにrefで持っておく
  const refValue = useRef<Type | undefined>(undefined);
  // useEffectのdependenciesに入れないようにするためにrefで持っておく
  const refCallback = useRef<
    (newValue: Type, prevValue: Type | undefined) => void
  >(callback);

  // mount時の処理
  useEffect(() => {
    if (option.immediate) {
      callback(watchingState, undefined);
    }
    refValue.current = watchingState;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  refCallback.current = callback;

  // 値に変化はないのかチェック
  useEffect(() => {
    if (refValue.current !== watchingState) {
      refCallback.current(watchingState, refValue.current);
      refValue.current = watchingState;
    }
  }, [watchingState]);
};

持続的に値を保持するために ref で値と cb を持っておく

refValuerefCallbackはそれぞれ現在の State の値とコールバック関数を保持するためのrefです。refstateとは異なり、refの値を更新しても再レンダリングされずに済みます。

mount 時の処理

最初のuseEffectは、コンポーネントがマウントされた時に一度だけ実行されます。optionimmediatetrueの場合は、コールバックが即座に実行されます。そして、refValueに初期の State の値がセットされます。

値に変化はないかのチェック

2 つ目のuseEffectは、監視対象の State が変更された場合に実行されます。State が前回の値と異なる場合、コールバック関数が実行され、新しい State の値と前の State の値が引数として渡されます。そして、refValueに最新の State の値がセットされます。

実際のコードは以下の codesandbox をご覧ください。

おわりに

以上が、React で Vue の Watch を再現する方法でした。弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!

https://www.wantedly.com/projects/1130967

https://www.wantedly.com/projects/768663

Discussion