デモ
App.js
import React, { useState, useEffect, useCallback } from "react";
export default function App() {
  const [state, setState] = useState("");
  const callback = useCallback(() => {
    //この中で更新後の値が使える
    console.log("debounced!!!", state);
  }, [state]);
  useEffect(() => {
    const handler = setTimeout(callback, 1000);
    //cleanup
    return () => {
      clearTimeout(handler);
    };
  },[callback);
  return (
    <div className="App">
      <input type="text" value={state} onChange={e => setState(e.target.value)} />
    </div>
  );
}
普通にlodashとか使って作ったdebounce関数をuseEffectにぶちこむと更新後のstateを使うためには引数として渡さなければいけないのですが、useEffectのcleanup関数をうまく使ってあげることでstateが変わりcallback関数が再作成されるたびcleanupの中のclearTimeoutが待機中だった処理をキャンセルして、effectの中で新しいsetTimeoutを作成することでdebounce処理を行うことができます。
customHook版
useDebouncedEffect.js
import { useEffect, useCallback } from "react";
const useDebounceeEffect = (effect, deps, delay) => {
  const callback = useCallback(effect, deps);
  useEffect(() => {
    const handler = setTimeout(callback, delay);
    //cleanup
    return () => {
      clearTimeout(handler);
    };
  }, [callback, delay]);
};
export default useDebounceeEffect;
App.js
import React, { useState } from "react";
import useDebouncedEffect from "./useDebouncedEffect";
export default function App() {
  const [text, setText] = useState("");
  useDebouncedEffect(
    () => {
      console.log("debounced!!!", text);
    },
    [text],
    1000
  );
  return (
    <div className="App">
      <input type="text" value={text} onChange={e => setText(e.target.value)} />
    </div>
  );
}
これでかんたんにstateの更新と紐付けたdebounce処理ができます
やったぜ。
