React: Hooks: useState one-off callbacks

Created on 17 Feb 2019  Â·  6Comments  Â·  Source: facebook/react

Do you want to request a feature or report a bug?
Question / feature

I've been trying to handle a case where I need to execute a piece of code right after the state is set at a particular place in the code. I do understand I'm supposed to use useEffect to respond to changes in state, like so:

const [val, setVal] = useState(null);
useEffect(() => { /* handle changes to val here */ }, [val])

But the problem is, it will run on all changes made to val anywhere in the code. Without the second argument of setVal being a callback that'll run after the state is set, how can I execute something after a specific setVal function call sets the state ?

Question

Most helpful comment

I think usually you have two options here:

  1. Don't wait for setState to flush and just do the work you want immediately.
function handleClick() {
  setState(newValue);
  doSomethingWith(newValue);
}
  1. Use an effect that always fires together with a mutable ref to remember what to do.
const needsSubmit = useRef(false);

function handleClick() {
  setValue(newValue);
  needsSubmit.current = true;
}

useEffect(() => {
  if (needsSubmit.current) {
    needsSubmit.current = false;
    doSomethingWith(value);
  }
});

All 6 comments

useEffect can also do one-off run, if you pass an empty array as second argument to it. As noted here:
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument

Can you be more specific about why you need to execute code after state has been set? A real example would be helpful.

@gaearon It's for an autocomplete component I'm building with an auto-submit feature. The state of the text input changes upon user input and some other factors, but right after user picks one of the suggestions (which changes the text input state), i want to submit the form.

I didn't want to pass the new value to the submit method just for this particular case, I like the function to grab it from the state just like it does when it's called from anywhere else. So I need to make sure the new value is set before calling the setter function, which is what I was going to use the callback for.

const keyPressed = ({ event, items, activeIndex, setValue, getItemValue }) => {

  // the item action was taken on
  const item = isNil(activeIndex) ? null : items[activeIndex];

  ...

  // -- enter
  if (event.keyCode === 13) {

    // `setValue` is the state setter function passed to
    // the Autocomplete component as a prop. So the state
    // isn't contained by the Autocomplete component,
    // it just manages it. I like to be able to the call the
    // submit function  after `setValue` is done here
    if (item) setValue(getItemValue(item));

    return true;
  }

  ...

}

I think usually you have two options here:

  1. Don't wait for setState to flush and just do the work you want immediately.
function handleClick() {
  setState(newValue);
  doSomethingWith(newValue);
}
  1. Use an effect that always fires together with a mutable ref to remember what to do.
const needsSubmit = useRef(false);

function handleClick() {
  setValue(newValue);
  needsSubmit.current = true;
}

useEffect(() => {
  if (needsSubmit.current) {
    needsSubmit.current = false;
    doSomethingWith(value);
  }
});

Thanks for the suggestions @gaearon.

Just out of curiosity, is there a particular reason why the setState callback practice wasn't carried over to the hooks ?

There’s a few reasons. Mostly that it doesn’t fit React model in general and Hooks make it more noticeable. That manifests in a few ways.

For example a callback would “see” previous state and props because that’s where it was defined. That seems weird when you goal is to wait for state to be updated.

It’s also not clear what semantics should be for skipped renders due to batching (which will happen more in concurrent mode). Should it only fire for those renders that were committed individually? Or should it fire once for every setState attempt? You could write subtly broken code regardless of which semantics we chose. Neither seems clearly superior.

In general we’ve noticed most code attempting to wait for a cascade of updates is buggy. This logic is usually not properly interruptible and doesn’t take into account what happens when several such events happen in quick succession. So even if we supported it, it would be a bad pattern.

Was this page helpful?
0 / 5 - 0 ratings