51

I have a form with username input and I am trying to verify if the username is in use or not in a debounce function. The issue I'm having is that my debounce doesn't seem to be working as when I type "user" my console looks like

u
us
use
user

Here is my debounce function

export function debounce(func, wait, immediate) {
    var timeout;

    return () => {
        var context = this, args = arguments;

        var later = () => {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };

        var callNow = immediate && !timeout;

        clearTimeout(timeout);

        timeout = setTimeout(later, wait);

        if (callNow) func.apply(context, args);
    };
};

And here is how I'm calling it in my React component

import React, { useEffect } from 'react' 

// verify username
useEffect(() => {
    if(state.username !== "") {
        verify();
    }
}, [state.username])

const verify = debounce(() => {
    console.log(state.username)
}, 1000);

The debounce function seems to be correct? Is there a problem with how I am calling it in react?

Ryne
  • 1,195
  • 2
  • 14
  • 32

5 Answers5

94

Every time your component re-renders, a new debounced verify function is created, which means that inside useEffect you are actually calling different functions which defeats the purpose of debouncing.

It's like you were doing something like this:

const debounced1 = debounce(() => { console.log(state.username) }, 1000);
debounced1();

const debounced2 = debounce(() => { console.log(state.username) }, 1000);
debounced2();

const debounced3 = debounce(() => { console.log(state.username) }, 1000);
debounced3();

as opposed to what you really want:

const debounced = debounce(() => { console.log(state.username) }, 1000);
debounced();
debounced();
debounced();

One way to solve this is to use useCallback which will always return the same callback (when you pass in an empty array as a second argument). Also, I would pass the username to this function instead of accessing the state inside (otherwise you will be accessing a stale state):

import { useCallback } from "react";
const App => () {
  const [username, setUsername] = useState("");

  useEffect(() => {
    if (username !== "") {
      verify(username);
    }
  }, [username]);

  const verify = useCallback(
    debounce(name => {
      console.log(name);
    }, 200),
    []
  );

  return <input onChange={e => setUsername(e.target.value)} />;
}

Also you need to slightly update your debounce function since it's not passing arguments correctly to the debounced function.

function debounce(func, wait, immediate) {
  var timeout;

  return (...args) => { <--- needs to use this `args` instead of the ones belonging to the enclosing scope
    var context = this;
...

demo

Note: You will see an ESLint warning about how useCallback expects an inline function, you can get around this by using useMemo knowing that useCallback(fn, deps) is equivalent to useMemo(() => fn, deps):

const verify = useMemo(
  () => debounce(name => {
    console.log(name);
  }, 200),
  []
);
isherwood
  • 58,414
  • 16
  • 114
  • 157
Hamza El Aoutar
  • 5,292
  • 2
  • 17
  • 23
19
export function useLazyEffect(effect: EffectCallback, deps: DependencyList = [], wait = 300) {
  const cleanUp = useRef<void | (() => void)>();
  const effectRef = useRef<EffectCallback>();
  const updatedEffect = useCallback(effect, deps);
  effectRef.current = updatedEffect;
  const lazyEffect = useCallback(
    _.debounce(() => {
      cleanUp.current = effectRef.current?.();
    }, wait),
    [],
  );
  useEffect(lazyEffect, deps);
  useEffect(() => {
    return () => {
      cleanUp.current instanceof Function ? cleanUp.current() : undefined;
    };
  }, []);
}
isherwood
  • 58,414
  • 16
  • 114
  • 157
shadow
  • 224
  • 2
  • 3
  • 2
    Nice generic version! This answer deserves more upvotes. Maybe add an example on how to use it (even though it may seem obvious). And maybe add some explanatory notes. – Christiaan Westerbeek Jun 07 '22 at 08:01
  • 2
    Heres that function wrapped in a nice gist with imports and ready to use :) https://gist.github.com/felipecsl/afb987f8b6059814cff0a2ca6020e108 – Felipe Lima Sep 08 '22 at 14:50
  • 1
    Some explanation would make this answer better, and it's up to the reader to decide whether it's "better". – isherwood Sep 15 '22 at 13:23
17

I suggest a few changes.

1) Every time you make a state change, you trigger a render. Every render has its own props and effects. So your useEffect is generating a new debounce function every time you update username. This is a good case for useCallback hooks to keep the function instance the same between renders, or possibly useRef maybe - I stick with useCallback myself.

2) I would separate out individual handlers instead of using useEffect to trigger your debounce - you end up with having a long list of dependencies as your component grows and it's not the best place for this.

3) Your debounce function doesn't deal with params. (I replaced with lodash.debouce, but you can debug your implementation)

4) I think you still want to update the state on keypress, but only run your denounced function every x secs

Example:

import React, { useState, useCallback } from "react";
import "./styles.css";
import debounce from "lodash.debounce";

export default function App() {
  const [username, setUsername] = useState('');

  const verify = useCallback(
    debounce(username => {
      console.log(`processing ${username}`);
    }, 1000),
    []
  );

  const handleUsernameChange = event => {
    setUsername(event.target.value);
    verify(event.target.value);
  };

  return (
    <div className="App">
      <h1>Debounce</h1>
      <input type="text" value={username} onChange={handleUsernameChange} />
    </div>
  );
}

DEMO

I highly recommend reading this great post on useEffect and hooks.

Samuel Goldenbaum
  • 18,391
  • 17
  • 66
  • 104
  • Would you suggest breaking up [state, setState] = { step: 1, name: "", email: "", username: "", password: "", confirm: "", error: "", verified: "", valid: false } into individual useStates? I understand why doing so for handling the username and the debounce. – Ryne May 14 '20 at 19:24
  • Yes, I would separate the state and relevant handlers so you have `[username, setUsername] handleUsernameChange()` `[password, setPassword] handlePasswordChange()` etc – Samuel Goldenbaum May 14 '20 at 20:02
0

A simple debounce functionality with useEffect,useState hooks

import {useState, useEffect} from 'react';

export default function DebounceInput(props) {
    const [timeoutId, setTimeoutId] = useState();

    useEffect(() => {
        return () => {
            clearTimeout(timeoutId);
        };
    }, [timeoutId]);

    function inputHandler(...args) {
        setTimeoutId(
            setTimeout(() => {
                getInputText(...args);
            }, 250)
        );
    }

    function getInputText(e) {
        console.log(e.target.value || "Hello World!!!");
    }

    return (
        <>
            <input type="text" onKeyDown={inputHandler} />
        </>
    );
}

I hope this works well. And below I attached vanilla js code for debounce

function debounce(cb, delay) {
    let timerId;
    return (...args) => {
        if (timerId) clearTimeout(timerId);
        timerId = setTimeout(() => {
            cb(...args);
        }, delay);
    };
}

function getInputText(e){
    console.log(e.target.value);
}

const input = document.querySelector('input');
input.addEventListener('keydown',debounce(getInputText,500));
karthik
  • 1
  • 1
-1

Here's a custom hook in plain JavaScript that will achieve a debounced useEffect:

export const useDebounce = (func, timeout=100) => {
    let timer;
    let deferred = () => {
        clearTimeout(timer); 
        timer = setTimeout(func, timeout);
    };
    const ref = useRef(deferred);
    return ref.current;
};

export const useDebouncedEffect = (func, deps=[], timeout=100) => {
    useEffect(useDebounce(func, timeout), deps);
}

For your example, you could use it like this:

useDebouncedEffect(() => {
    if(state.username !== "") {
        console.log(state.username);
    }
}, [state.username])
Richard Fairhurst
  • 816
  • 1
  • 7
  • 15