2

Trying to create a delay on react component that has input field that updates on change.

Here's the code:

import React from 'react'; 
import Input from "@material-ui/core/Input";
import {debounce} from "lodash";
...

const SampleRow = () => {
    let [newFriend, setNewFriend] = React.useState({name: '', lastSeen: '', contactIn: ''});

    const onAddFriend = debounce(() => {
        if(newFriend.name.length === 0){
            return;
        }
        console.log(newFriend.name);
    }, 2000);

    return (
        <TableRow>
            <TableCell component="th" scope="row">
                <Input
                    value={newFriend.name}
                    onChange={(event) => {
                        setNewFriend({...newFriend, name: event.target.value});
                        onAddFriend();
                    }}
                    placeholder={"Add friend"}
                    disableUnderline={true}
                />
            </TableCell>
            <TableCell align="left">{newFriend.lastSeen ? newFriend.lastSeen : ''}</TableCell>
            <TableCell align="left">{newFriend.contactIn ? newFriend.contactIn : ''}</TableCell>
            <TableCell align="left" className={classes.externalLinks}/>
        </TableRow>
    )
};

What happens is that the console.log prints out every change (after the timeout of 2s), and won't cancel the previous triggers.

The question is why is that happens? and how could this could be solved?

I've found similar questions with complex useDebounce logic or class-components. Didn't find a reference for this problem.

YanivGK
  • 893
  • 12
  • 18
  • you will have to clear the debounce function already been invoked. – Sheelpriy Jul 16 '20 at 16:27
  • Isn't that the whole concept of `debounce`? recalling the function should reset the `time-interval`. – YanivGK Jul 16 '20 at 18:07
  • I think the rendering of the function might cause the problem. I tried to `event.persist()` but that wasn't it. – YanivGK Jul 16 '20 at 18:11
  • 1
    This is precisely why the solutions you found are "complicated". With hooks, you need to use `useRef` for storing/handling/referencing the callback. This is how existing hooks like useDebouncedCallback approach it. There is a great [article](https://overreacted.io/making-setinterval-declarative-with-react-hooks/) on `useRef` with functionality like useInterval. – Alexander Staroselsky Jul 16 '20 at 18:25

3 Answers3

2

The issue here is that your debounced function is being recreated on each render.

One way to solve this is to move it out of the functional component. Another way is to change your component to a Class component and save your debounced function as a property.

const { useState } = React
const { debounce } = _;

const onAddFriend = debounce(console.log, 2000);

const App = () => {
 
  const [newFriend, setNewFriend] = useState({name: '',   lastSeen: '', contactIn: ''});
  
  const handleInput = (event) => {
    setNewFriend({...newFriend, name: event.target.value});
    onAddFriend(event.target.value);
  }
  
  return (
    <div className="box">
      <form className="form">
        <div class="field">
          <label for="name-1">Update  {newFriend.name}</label>
          <div class="control">
            <input type="text" name="name-1" value={newFriend.name}
                    onChange={handleInput} class="input"/>
          </div>
        </div>
      </form>
    </div>
  )
}

ReactDOM.render(<App />,
document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.19/lodash.min.js"></script>
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.production.min.js"></script>

<div id="root"></div>
zlace
  • 595
  • 3
  • 13
  • I thought later on recreating the function may cause the problem, but didn't though of exporting it as an external function for solution - nice, simple, and working as expected - thanks! – YanivGK Jul 16 '20 at 21:32
0

Ciao, try to cancel the prevoius trigger:

import React from 'react'; 
import Input from "@material-ui/core/Input";
import {debounce} from "lodash";
...

const SampleRow = () => {
  let [newFriend, setNewFriend] = React.useState({name: '', lastSeen: '', contactIn: 
''});

const onAddFriend = debounce((abort) => {
    if(abort){
        return;
    }
    console.log(newFriend.name);
}, 2000);

return (
    <TableRow>
        <TableCell component="th" scope="row">
            <Input
                value={newFriend.name}
                onChange={(event) => {
                    setNewFriend({...newFriend, name: event.target.value});
                    let abort = false;
                    if(event.target.value.length === 0){
                      abort = true;
                    }
                    onAddFriend(abort);
                }}
                placeholder={"Add friend"}
                disableUnderline={true}
            />
        </TableCell>
        <TableCell align="left">{newFriend.lastSeen ? newFriend.lastSeen : ''}</TableCell>
        <TableCell align="left">{newFriend.contactIn ? newFriend.contactIn : ''}</TableCell>
        <TableCell align="left" className={classes.externalLinks}/>
    </TableRow>
)
  };
Giovanni Esposito
  • 10,696
  • 1
  • 14
  • 30
  • It doesn't work. The last trigger is canceled, but the rest are still active (and printed to the console after the given duration). – YanivGK Jul 16 '20 at 15:50
  • Ok, another solution I konw is a solution with a flag. I will update my answer in a while. – Giovanni Esposito Jul 16 '20 at 16:01
  • Unfortunately it doesn't work. I do get the idea behind of it, but my guess is that the re-rendering of the `onAddFriend` func cause it to miss the flag. – YanivGK Jul 16 '20 at 18:14
0

Try this. It will always check existing debounced in the queue, and cancel it finds any. Refer : How to cancel a debounced function after it is called and before it executes?

Sheelpriy
  • 1,675
  • 17
  • 28