1

I have a component in React that essentially autosaves form input 3 seconds after the user's last keystroke. There are possibly dozens of these components rendered on my webpage at a time.

I have tried using debounce from Lodash, but that did not seem to work (or I implemented it poorly). Instead, I am using a function that compares a local variable against a global variable to determine the most recent function call.

Curiously, this code seems to work in JSFiddle. However, it does not work on my Desktop.

Specifically, globalEditIndex seems to retain its older values even after the delay. As a result, if a user makes 5 keystrokes, the console.log statement runs 5 times instead of 1.

Could someone please help me figure out why?

import React, {useRef, useState} from "react";
import {connect} from "react-redux";

import {func1, func2} from "../actions";

// A component with some JSX form elements. This component shows up dozens of times on a single page
const MyComponent = (props) => {

    // Used to store the form's state for Redux
    const [formState, setFormState] = useState({});
    
    // Global variable that keeps track of number of keystrokes
    let globalEditIndex = 0;
    
    // This function is called whenever a form input is changed (onchange)
    const editHandler = (e) => {
        setFormState({
            ...formState,
            e.target.name: e.target.value,
        });

        autosaveHandler();
    }
    
    const autosaveHandler = () => {
        globalEditIndex++;
        let localEditIndex = globalEditIndex;
        setTimeout(() => {

            // Ideally, subsequent function calls increment globalEditIndex, 
            // causing earlier function calls to evaluate this expression as false.
            if (localEditIndex === globalEditIndex) {
                console.log("After save: " +globalEditIndex);
                globalEditIndex = 0;
            }

        }, 3000);
    }

    return(
        // JSX code here
    )
}

const mapStateToProps = (state) => ({
    prop1: state.prop1,
    prop2: state.prop2
});

export default connect(
    mapStateToProps, { func1, func2 }
)(MyComponent);
nucleic550
  • 85
  • 1
  • 6
  • Just to be clear, basically you just want to debounce form's input value, correct? This can be easily handled with a proper custom hook. – ivanatias Jul 25 '22 at 23:06
  • You did imported useRef from react, but what stopped you from using it `const globalEditIndexRef = useRef(0);` and then `globalEditIndexRef.current++`, `let localEditIndex = globalEditIndexRef.current;` and etc? In your function your variables are closure-captured, in setTimeout callback - they are still same closure-captured – Sergey Sosunov Jul 25 '22 at 23:06
  • 1
    Does this help answer your question? https://stackoverflow.com/a/70270521/8690857 Or are you trying to create *some* globally debounced handler? It is unclear what you mean exactly by "There are possibly dozens of these components rendered on my webpage at a time." Are you trying to collectively debounce *all* of them? Are they all rendered to the DOM and/or interacted with simultaneously? – Drew Reese Jul 25 '22 at 23:07

1 Answers1

1

Note: I was typing up answer on how I solved this previously in my own projects before I read @DrewReese's comment - that seems like a way better implementation than what I did, and I will be using that myself going forward. Check out his answer here: https://stackoverflow.com/a/70270521/8690857


I think you hit it on the head in your question - you are probably trying to implement debounce wrong. You should debounce your formState value to whatever delay you want to put on autosaving (if I'm assuming the code correctly).

An example custom hook I've used in the past looks like this:

export const useDebounce = <T>(value: T, delay: number) => {
    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => {
        const timer = setTimeout(() => setDebouncedValue(value), delay);
        return () => clearTimeout(timer);
    }, [value]);

    return debouncedValue;
};

// Without typings
const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
};

Which you can then use like so:

  const [myValue, setMyValue] = useState<number>(0);

  const debouncedValue = useDebounce<number>(myValue, 3000);

  useEffect(() => {
    console.log("Debounced value: ", debouncedValue);
  }, [debouncedFormState]);

  // without typings
  const [myValue, setMyValue] = useState(0);

  const debouncedValue = useDebounce(myValue, 3000);

  useEffect(() => {
    console.log("Debounced value: ", debouncedValue);
  }, [debouncedFormState]);

For demonstration purposes I've made a CodeSandbox demonstrating this useDebounce function with your forms example. You can view it here: https://codesandbox.io/s/brave-wilson-cjl85v?file=/src/App.js

nbokmans
  • 5,492
  • 4
  • 35
  • 59
  • This works well, but I find that whenever I perform a state update with Redux, the component _constantly_ fires the debounce function. Why might this be? – nucleic550 Jul 26 '22 at 21:45