Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

useEffect with debounce

I'm trying to create an input field that has its value de-bounced (to avoid unnecessary server trips). The first time I render my component I fetch its value from the server (there is a loading state and all).

Here is what I have (I omitted the irrelevant code, for the purpose of the example).

This is my debounce hook:

export function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

(I got this from: https://usehooks.com/useDebounce/)

Right, here is my component and how I use the useDebounce hook:

function ExampleTitleInput(props) {
  const [title, setTitle] = useState(props.title || "");
  const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
  const [commitsCount, setCommitsCount] = useState(0);

  const debouncedTitle = useDebounce(title, 1000);

  useEffect(() => {
    setTitle(props.title || "");
  }, [props.title]);

  useEffect(() => {
    if (debouncedTitle !== lastCommittedTitle) {
      setLastCommittedTitle(debouncedTitle);
      setCommitsCount(commitsCount + 1);
    }
  }, [debouncedTitle, lastCommittedTitle, commitsCount]);

  return (
    <div className="example-input-container">
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <div>Last Committed Value: {lastCommittedTitle}</div>
      <div>Commits: {commitsCount}</div>
    </div>
  );
}

Here is the parent component:

function App() {
  const [title, setTitle] = useState("");

  useEffect(() => {
    setTimeout(() => setTitle("This came async from the server"), 2000);
  }, []);

  return (
    <div className="App">
      <h1>Example</h1>
      <ExampleTitleInput title={title} />
    </div>
  );
}

When I run this code, I would like it to ignore the debounce value change the first time around (only), so it should show that the number of commits are 0, because the value is passed from the props. Any other change should be tracked. Sorry I've had a long day and I'm a bit confused at this point (I've been staring at this "problem" for far too long I think).

I've created a sample:

https://codesandbox.io/s/zen-dust-mih5d

It should show the number of commits being 0 and the value set correctly without the debounce to change.

I hope I'm making sense, please let me know if I can provide more info.

Edit

This works exactly as I expect it, however it's giving me "warnings" (notice dependencies are missing from the deps array):

function ExampleTitleInput(props) {
  const [title, setTitle] = useState(props.title || "");
  const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
  const [commitsCount, setCommitsCount] = useState(0);

  const debouncedTitle = useDebounce(title, 1000);

  useEffect(() => {
    setTitle(props.title || "");
    // I added this line here
    setLastCommittedTitle(props.title || "");
  }, [props]);

  useEffect(() => {
    if (debouncedTitle !== lastCommittedTitle) {
      setLastCommittedTitle(debouncedTitle);
      setCommitsCount(commitsCount + 1);
    }
  }, [debouncedTitle]); // removed the rest of the dependencies here, but now eslint is complaining and giving me a warning that I use dependencies that are not listed in the deps array

  return (
    <div className="example-input-container">
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <div>Last Committed Value: {lastCommittedTitle}</div>
      <div>Commits: {commitsCount}</div>
    </div>
  );
}

Here it is: https://codesandbox.io/s/optimistic-perlman-w8uug

This works, fine, but I'm worried about the warning, it feels like I'm doing something wrong.

like image 570
Dimitar Dimitrov Avatar asked Aug 18 '19 19:08

Dimitar Dimitrov


People also ask

How do you debounce in Reactjs?

In the case of Debouncing, the API will trigger only once after 2 seconds, after we type our whole pin-code. First of all, create a state using the useState hook in React. const [pinCode, setPinCode] = React. useState("");

Can you pass arguments to useEffect?

useEffect takes two arguments. The first argument passed to useEffect is a function called effect and the second argument (optional) is an array of dependencies. Below is an example. import { useEffect } from "react"; import { render } from "react-dom"; const App = (props) => { useEffect(() => { console.

Can I use useEffect with props?

Just like the state, we can also use props as a dependency to run the side effect. In this case, the side effect will run every time there is a change to the props passed as a dependency. useEffect(() => { // Side Effect }, [props]);

Does useEffect run sequentially?

This is no surprise while working with useEffect because it executes in the order sequentially as described in the code block. Note that depending on which effect function is triggered, different texts are logged, and as intended, the first impact mechanism is prompted before the second.


2 Answers

A simple way to check if we are in the first render is to set a variable that changes at the end of the cycle. You could achieve this using a ref inside your component:

const myComponent = () => {
    const is_first_render = useRef(true);

    useEffect(() => {
        is_first_render.current = false;
    }, []);

    // ...

You can extract it into a hook and simply import it in your component:

const useIsFirstRender = () => {
    const is_first_render = useRef(true);

    useEffect(() => {
        is_first_render.current = false;
    }, []);

    return is_first_render.current;
};

Then in your component:

function ExampleTitleInput(props) {
    const [title, setTitle] = useState(props.title || "");
    const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
    const [updatesCount, setUpdatesCount] = useState(0);
    const is_first_render = useIsFirstRender(); // Here

    const debouncedTitle = useDebounce(title, 1000);

    useEffect(() => {
        setTitle(props.title || "");
    }, [props.title]);

    useEffect(() => {
        // I don't want this to trigger when the value is passed by the props (i.e. - when initialized)
        if (is_first_render) { // Here
            return;
        }

        if (debouncedTitle !== lastCommittedTitle) {
            setLastCommittedTitle(debouncedTitle);
            setUpdatesCount(updatesCount + 1);
        }
    }, [debouncedTitle, lastCommittedTitle, updatesCount]);

    // ...
like image 155
Alvaro Avatar answered Sep 27 '22 20:09

Alvaro


You can change the useDebounce hook to be aware of the fact that the first set debounce value should be set immediately. useRef is perfect for that:

export function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  const firstDebounce = useRef(true);

  useEffect(() => {
    if (value && firstDebounce.current) {
      setDebouncedValue(value);
      firstDebounce.current = false;
      return;
    }

    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}
like image 31
Federkun Avatar answered Sep 27 '22 19:09

Federkun