Debounce で値を反映したいコンポーネントを作っていて、setTimeout
内で event から渡された event.target
を使おうとしたら null
になっていてエラーになり、なんもわからん…
になってハマっていたのでメモ
event.target
が null になるコード
useDebounceCallback.js
import { useRef, useCallback } from 'react'; export const useDebounceCallback = (callback, delay = 300) => { const debounceTimer = useRef(null); const dispatch = useCallback((...args) => { debounceTimer.current && clearTimeout(debounceTimer.current); debounceTimer.current = setTimeout(() => { return callback(...args); }, delay); }, [callback, delay]); const cancel = useCallback(() => { debounceTimer.current && clearTimeout(debounceTimer.current); }, []); return [dispatch, cancel]; };
App.js
import React, { useState, useCallback } from 'react'; import useDebounceCallback from './useDebounceCallback'; const App = ({ initValue }) => { const [value, setValue] = useState(initValue); const updateValue = useCallback((e) => { setValue(e.target.value); }, []); const [onDebounceUpdateValue] = useDebounceCallback(updateValue, 300); return ( <> <p>{value}</p> <input onChange={onDebounceUpdateValue} defaultValue={initValue} /> </> ); }
これで input の値を変更しようとすると setValue(e.target.value)
の部分で、TypeError: Cannot read property 'value' of null
というエラーが発生してしまいます。
イベント内に console.log()
を置いてみると次のような感じになっています。
const updateValue = useCallback((e) => { console.log(e.target); // null setValue(e.target.value); }, []);
e.target
そのものが null
になっているので、e.target.value
にアクセスしようとすると JavaScript のエラーが発生してしまうというもの。
イベントで渡される引数を渡しているだけなのに、WHY…
React ではイベントで SyntheticEvent
というネイティブのイベントをラップしたインスタンスが渡される
SyntheticEvent
では callback イベントが発火し終わると、プロパティは null
になる仕様になっているようです。
Event Pooling
TheSyntheticEvent
is pooled. This means that theSyntheticEvent
object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.
cf. SyntheticEvent – React
先の例は custom hooks を使ったりで複雑になっているので、問題をシンプルにしてみます。
import React, { useState } from 'react'; const App = ({ initValue }) => { const [value, setValue] = useState(initValue); const onDebounceUpdateValue = (e) => { console.log(e, e.target); // => SyntheticEvent, <input /> setTimeout(() => { console.log(e.target); // => null; setValue(e.target.value); // => TypeError: Cannot read property 'value' of null }, 300); // ここで callback イベントは終了して SyntheticEvent のプロパティは nullified される }; return ( <> <p>{value}</p> <input onChange={onDebounceUpdateValue} defaultValue={initValue} /> </> ); }
解決方法: 非同期イベント外で変数に使用したい値を格納しておく
const App = ({ initValue }) => { const [value, setValue] = useState(initValue); const onDebounceUpdateValue = (e) => { const target = e.target; setTimeout(() => { console.log(target); // => <input />; setValue(target.value); // => 👌 }, 300); }; // 略 }
🙅 async / await をしても SyntheticEvent のプロパティは nullified されるのでダメ
非同期イベントを async / await すれば大丈夫なのではと思ったのですが、非同期イベント内では null 化されたものが渡されるようです。
const App = ({ initValue }) => { const [value, setValue] = useState(initValue); const onDebounceUpdateValue = async (e) => { await new Promise((resolve) => { return setTimeout(() => { console.log(e.target); // => null 😇 setValue(e.target.value); // => TypeError: Cannot read property 'value' of null return resolve(); }, 1000); }); console.log('callback end'); }; // 略 }
最初の useDebounceCallback hooks を使ってる場合はどう修正すればよいのか
custom hook は使いまわしたいので、custom hook 内で const target = e.target
の様な事をするのは避けたいところです。
なので、custom hook が返す関数をイベントでそのまま呼び出すのではなく、コンポーネント内でもうひとつ関数で囲ってやればOK。
useDebounceCallback.js
変更なし
import { useRef, useCallback } from 'react'; export const useDebounceCallback = (callback, delay = 300) => { const debounceTimer = useRef(null); const dispatch = useCallback((...args) => { debounceTimer.current && clearTimeout(debounceTimer.current); debounceTimer.current = setTimeout(() => { return callback(...args); }, delay); }, [callback, delay]); const cancel = useCallback(() => { debounceTimer.current && clearTimeout(debounceTimer.current); }, []); return [dispatch, cancel]; };
App.js
import React, { useState, useCallback } from 'react'; import useDebounceCallback from './useDebounceCallback'; const App = ({ initValue }) => { const [value, setValue] = useState(initValue); const updateValue = useCallback((value) => setValue(value), []); const [debounceUpdateValue] = useDebounceCallback(updateValue, 300); const onDebounceUpdateValue = useCallback((e) => { debounceUpdateValue(e.target.value); // 必要な値だけを渡してあげる }, [debounceUpdateValue]); return ( <> <p>{value}</p> <input onChange={onDebounceUpdateValue} defaultValue={initValue} /> </> ); }
完 全
理 解
₍ᐢ › ༝ ‹ ᐢ₎
note.
nullified
… 無効化invoke
…発動する
asynchronous
… 非同期な
[参考]

みんなでアジャイル ―変化に対応できる顧客中心組織のつくりかた
- 作者:Matt LeMay
- 発売日: 2020/03/19
- メディア: 単行本(ソフトカバー)