Debouncing multiple calls with useCallback in React for independent updates

179 views Asked by At

I'm facing a challenge with debouncing multiple calls to a function in a React component, where each call represents an update to the number of copies for a specific file. To implement debouncing, I'm using the debounce method from the lodash library and the useCallback hook to prevent unnecessary re-renders. Here's the current code snippet:

  const updateFileCopies = async (params: addReduceCopiesParams) => {
      return addReduceCopiesMutation.mutateAsync(params);
   }

  const debouncedFileCopiesUpdate = useCallback(_debounce(updateFileCopies, 1000), []);

  const addReduceCopies = async (file: OrderDocumentProps, isAdd = false) => {
     /** here I find the file and update the UI state ...  **/

     debouncedFileCopiesUpdate({
      fileId: file.id as string,
      copies: newCopiesNumber
    });
  }

The issue arises when a user rapidly clicks the add/reduce buttons for different files. The current implementation debounces all calls, leading to only the last call for a specific file being sent to the server. Other files don't get updated.

For example, with the following sequence of calls:

addReduceCopies({ id: 1, isAdd: true })

addReduceCopies({ id: 1, isAdd: true })

addReduceCopies({ id: 1, isAdd: true })

addReduceCopies({ id: 2, isAdd: true })

addReduceCopies({ id: 2, isAdd: true })

addReduceCopies({ id: 1, isAdd: true })

Only the last call for file with id 1 gets processed, and the file with id 2 doesn't get updated.

How can I modify the existing code to implement separate debouncing for each file, ensuring that all updates are processed independently?

1

There are 1 answers

0
Ori Drori On

You can create a function that holds a Map of keys, and their respective debounced functions.

Note: one thing to consider is that the there will be a debounced function for each key, so the internal map would keep growing even if the key doesn't appear anymore. To prevent that, as soon as the wrapped function is invoked, we also delete it's key from the Map. If the key would appear again, a new debounced function would be created and used until the wrapped function is invoked.

// debounceOptions are the standard options for _.debounce
// getKey is a function that extracts the key to use from the arguments that the debounce function is called with
const debounceByKey = (fn, wait, { getKey = v => v, ...debounceOptions } = {}) => {
  const debouncedMap = new Map()
  
  // wrap the function invoke
  const runFn = (key, ...args) => {
    fn(...args)
    
    // delete the key with the debounced fn. It will be recreated if needed again
    debouncedMap.delete(key)
  }
  
  const debouncedFn = (...args) => {
    // generate the key from the args
    const key = getKey(...args)
    
    if(!debouncedMap.has(key)) { // get a new debounce function for key if none exists
      debouncedMap.set(key, _.debounce(runFn, wait, debounceOptions))
    }
    
    // get the debounced function for the key
    const fnByKey = debouncedMap.get(key)
    
    // call it with original args
    fnByKey(key, ...args)
  }
  
  // maintain support for _.debounce() cancel and flush
  debouncedFn.cancel = () => debouncedMap.forEach(f => f.cancel())
  debouncedFn.flush = () => debouncedMap.forEach(f => f.flush)
  
  return debouncedFn
}

const updateFileCopies = obj => console.log(obj)

const addReduceCopies = debounceByKey(updateFileCopies, 1000, {
  getKey: obj => obj.id
})

addReduceCopies({ id: 1, isAdd: true, n: 1 })
addReduceCopies({ id: 1, isAdd: true, n: 2 })
addReduceCopies({ id: 1, isAdd: true, n: 3 })
addReduceCopies({ id: 2, isAdd: true, n: 1 })
addReduceCopies({ id: 2, isAdd: true, n: 2 })
addReduceCopies({ id: 1, isAdd: true, n: 4 })
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>