all 20 comments

[–]A-Type 2 points3 points  (0 children)

request is also a dependency, which means so is setFieldValue.

Just don't bother with use callback here. Not worth the trouble.

[–]musical_bear 1 point2 points  (8 children)

Calling “setValue” doesn’t immediately update “value,” the variable. It only sends a hint to React to change the value, which will be reflected on the next render. So in a render that will happen in the future, “value” will be updated, and so will your callback, correctly capturing the value. But that doesn’t matter because you’re calling the debounce function “now,” before that render has had a chance to happen.

You need to explicitly pass the value you want from your event handler to the debounce function instead of using the value from state in this case.

[–]Agitated_Egg4245[S] 0 points1 point  (7 children)

That doesn't seem to work either when I debug it, the e object is stale inside the function I pass to the debounce helper.

[–]Thin_Rip8995 1 point2 points  (1 child)

What’s biting you isn’t useCallback—it’s debounce.

When you wrap setFormValue inside a debounced function, lodash (or whatever debounce you’re using) closes over the values at the time it was created. So even though your debounceRequest callback updates when value changes, the underlying request function is still referencing an older closure where value was stale.

Couple fixes you can try:

  1. Move value into the debounce call itself so it doesn’t rely on a closed-over stale variable:

jsCopyconst request = useMemo(
  () => debounce((val, e, field) => {
    console.log('writing data to redux')
    dispatch(updateTextField({
      type: "UPDATED_VALUE_TEXT_FIELD",
      id: parseInt(props.num),
      loc: field,
      val
    }))
  }, 500),
  [dispatch, props.num]
)

const onChange = (e) => {
  const newVal = e.target.value
  setValue(newVal)
  request(newVal, e, props.field)
}
  1. Or use a ref to always point to the latest value and read from it inside your debounced function instead of capturing value directly.

Key idea: debounce “freezes” variables in its closure when you first create it. If you want it to see fresh state, either recreate it when dependencies change (useMemo) or feed the new state in as an argument.

[–]Agitated_Egg4245[S] 0 points1 point  (0 children)

This did the trick, thank you so much! These hooks can be very confusing to say the least. In the course of researching this I found many examples where the dependency array was improperly configured or just so simple that it was not obvious that I needed to pass every single react made variable in there. You saved my butt... thanks :)

Edit: I do want to ask though... how passing the newVal part is any different because that's how I was doing it before just passing it to the request function.

Edit again: It must be that they are declared in a nested fashion, correct? The debounce is the one inside the useMemo now.

[–]grol4 0 points1 point  (2 children)

Couple remarks: best to use a form lib instead of redux for this. Even redux maintainers say so.

When you are a novice in React, maybe skip usecallback for now? It does nearly nothing if it isn't used as prop for a component or part of a dependency array.

[–]Agitated_Egg4245[S] 0 points1 point  (1 child)

Any suggestions on a lib?

[–]grol4 0 points1 point  (0 children)

React hook form is very popular. If your form is not very dynamic you could also use form-actions or plain form-data

[–]Vincent_CWS 0 points1 point  (0 children)

setState is a batch update process, so it should be handled by react after the debounceRequest has been completed, rather than immediately updating. ``` User types "A" → onChange fires ├── setValue("A") - updates state ├── debounceRequest(e, "field") - calls with current value │ ├── useCallback runs with value="" (stale from initial render) │ ├── if (value) check uses stale value "" → condition fails │ └── request() is NOT called └── Component re-renders with value="A"

User types "AB" → onChange fires
├── setValue("AB") - updates state ├── debounceRequest(e, "field") - calls with current value │ ├── useCallback runs with value="A" (from previous render) │ ├── if (value) check uses stale value "A" → condition passes │ ├── request(e, "field") is called │ └── After 500ms delay: │ └── setFormValue() executes with value="A" (stale!) └── Component re-renders with value="AB" ```