VueのWatchをReactのカスタムフックで再現してみた
こんにちは! 株式会社 CastingONEの岡本です!
はじめに
弊社は現在、Vue から React への移行作業を進めています。この過程で、Vue で頻繁に利用していたウォッチャーのような機能が React にはデフォルトで存在しないことに気づきました。しかし、React の useRef
や useEffect
を駆使することで、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
カスタムフックのインターフェースは以下の通りです。
/**
* 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 はそもそも参照自体を新しくする書き方をするため、このオプションを設定するためのインターフェースは提供していません。
実装
以下が、カスタムフックの実装内容です。
/**
* 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 を持っておく
refValue
とrefCallback
はそれぞれ現在の State の値とコールバック関数を保持するためのref
です。ref
はstate
とは異なり、ref
の値を更新しても再レンダリングされずに済みます。
mount 時の処理
最初のuseEffect
は、コンポーネントがマウントされた時に一度だけ実行されます。option
のimmediate
がtrue
の場合は、コールバックが即座に実行されます。そして、refValue
に初期の State の値がセットされます。
値に変化はないかのチェック
2 つ目のuseEffect
は、監視対象の State が変更された場合に実行されます。State が前回の値と異なる場合、コールバック関数が実行され、新しい State の値と前の State の値が引数として渡されます。そして、refValue
に最新の State の値がセットされます。
実際のコードは以下の codesandbox をご覧ください。
おわりに
以上が、React で Vue の Watch を再現する方法でした。弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!
Discussion