Search code examples
reactjsautosavedebouncing

ReactJS - Autosave with multiple inputs - best practices


The goal I want to achieve is to implement the autosave function without hurting the performance (useless rerenders etc). Ideally, when the autosave will happen, the state will also be updated. I created an example component with 3 inputs, in this example the component rerenders on every keystroke. I also have a useEffect hook in which I'm looking for data changes and then I save them after 1sec. The ChildComponent is used to preview the input data.

function App(props) {
  
  const timer = React.useRef(null);  
  const [data, setData] = React.useState(props.inputData);

  React.useEffect(() => {
    clearTimeout(timer.current)
    timer.current = setTimeout(() => {
      console.log("Saving call...", data)
    }, 1000)
  }, [data])

  const inputChangeHandler = (e, type) => {
    if (type === "first") {
      setData({ ...data, first: e.target.value })
    } else if (type === "second") {
      setData({ ...data, second: e.target.value })
    } else if (type === "third") {
      setData({ ...data, third: e.target.value })
    }
  }

  return (
    <>
      <div className="inputFields">
        <input 
          defaultValue={data.first} 
          type="text" 
          onChange={(e) => inputChangeHandler(e, "first")} 
        />
        <input 
          defaultValue={data.second} 
          type="text" 
          onChange={(e) => inputChangeHandler(e, "second")} 
        />
        <input 
          defaultValue={data.third} 
          type="text" 
          onChange={(e) => inputChangeHandler(e, "third")} 
        />
      </div>
      <ChildComponent data={data} />
    </>
  )
}

I've read about debounce but my implementation didn't work. Has anyone run into the same problem?

Below is my debounce implementation using lodash:

React.useEffect(() => {
  console.log("Saving call...", data)
}, [data])

const delayedSave = React.useCallback(_.debounce(value => setData(value), 1000), []);

const inputChangeHandler = (e, type) => {
  if (type === "first") {
    let obj = { ...data };
    obj.first = e.target.value;
    delayedSave(obj)
  } else if (type === "second") {
    let obj = { ...data };
    obj.second = e.target.value;
    delayedSave(obj)
  } else if (type === "third") {
    let obj = { ...data };
    obj.third = e.target.value;
    delayedSave(obj)
  }
}

The problem with this one is that if a user types immediately (before the 1sec delay) from the first input to the second it only saves the last user input.


Solution

  • The problem in your implementation is that the timer is set in a closure (in useEffect) using the data your component had before the timer starts. You should start the timer once the data (or the newData in my implementation proposal) is changed. Something like:

    function App(props) {
      const [data, setData] = React.useState(props.inputData);
      const { current } = React.useRef({ data, timer: null });
    
      const inputChangeHandler = (e, type) => {
        current.data = { ...current.data, [type]: e.target.value };
    
        if(current.timer) clearTimeout(current.timer);
    
        current.timer = setTimeout(() => {
          current.timer = null;
          setData(current.data);
          console.log("Saving...", current.data);
        }, 1000);
      }
    
      return (
        <>
          <input defaultValue={data.first} type="text" onChange={(e) => inputChangeHandler(e, "first")} />
          <input defaultValue={data.second} type="text" onChange={(e) => inputChangeHandler(e, "second")} />
          <input defaultValue={data.third} type="text" onChange={(e) => inputChangeHandler(e, "third")} />
        </>
      );
    }