How to use `setState` callback on react hooks

246,491

Solution 1

You need to use useEffect hook to achieve this.

const [counter, setCounter] = useState(0);

const doSomething = () => {
  setCounter(123);
}

useEffect(() => {
   console.log('Do something after counter has changed', counter);
}, [counter]);

If you want the useEffect callback to be ignored during the first initial render, then modify the code accordingly:

import React, { useEffect, useRef } from 'react';

const [counter, setCounter] = useState(0);
const didMount = useRef(false);

const doSomething = () => {
  setCounter(123);
}

useEffect(() => {
  // Return early, if this is the first render:
  if ( !didMount.current ) {
    return didMount.current = true;
  }
  // Paste code to be executed on subsequent renders:
  console.log('Do something after counter has changed', counter);
}, [counter]);

Solution 2

If you want to update previous state then you can do like this in hooks:

const [count, setCount] = useState(0);


setCount(previousCount => previousCount + 1);

Solution 3

Mimic setState callback with useEffect, only firing on state updates (not initial state):

const [state, setState] = useState({ name: "Michael" })
const isFirstRender = useRef(true)
useEffect(() => {
  if (isFirstRender.current) {
    isFirstRender.current = false // toggle flag after first render/mounting
    return;
  }
  console.log(state) // do something after state has updated
}, [state])

Custom Hook useEffectUpdate

function useEffectUpdate(callback) {
  const isFirstRender = useRef(true);
  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false; // toggle flag after first render/mounting
      return;
    }
    callback(); // performing action after state has updated
  }, [callback]);
}

// client usage, given some state dep
const cb = useCallback(() => { console.log(state) }, [state]); // memoize callback
useEffectUpdate(cb);

Solution 4

I Think, using useEffect is not an intuitive way.

I created a wrapper for this. In this custom hook, you can transmit your callback to setState parameter instead of useState parameter.

I just created Typescript version. So if you need to use this in Javascript, just remove some type notation from code.

Usage

const [state, setState] = useStateCallback(1);
setState(2, (n) => {
  console.log(n) // 2
});

Declaration

import { SetStateAction, useCallback, useEffect, useRef, useState } from 'react';

type Callback<T> = (value?: T) => void;
type DispatchWithCallback<T> = (value: T, callback?: Callback<T>) => void;

function useStateCallback<T>(initialState: T | (() => T)): [T, DispatchWithCallback<SetStateAction<T>>] {
  const [state, _setState] = useState(initialState);

  const callbackRef = useRef<Callback<T>>();
  const isFirstCallbackCall = useRef<boolean>(true);

  const setState = useCallback((setStateAction: SetStateAction<T>, callback?: Callback<T>): void => {
    callbackRef.current = callback;
    _setState(setStateAction);
  }, []);

  useEffect(() => {
    if (isFirstCallbackCall.current) {
      isFirstCallbackCall.current = false;
      return;
    }
    callbackRef.current?.(state);
  }, [state]);

  return [state, setState];
}

export default useStateCallback;

Drawback

If the passed arrow function references a variable outer function, then it will capture current value not a value after the state is updated. In the above usage example, console.log(state) will print 1 not 2.

Solution 5

I was running into the same problem, using useEffect in my setup didn't do the trick (I'm updating a parent's state from an array multiple child components and I need to know which component updated the data).

Wrapping setState in a promise allows to trigger an arbitrary action after completion:

import React, {useState} from 'react'

function App() {
  const [count, setCount] = useState(0)

  function handleClick(){
    Promise.resolve()
      .then(() => { setCount(count => count+1)})
      .then(() => console.log(count))
  }


  return (
    <button onClick= {handleClick}> Increase counter </button>
  )
}

export default App;

The following question put me in the right direction: Does React batch state update functions when using hooks?

Share:
246,491

Related videos on Youtube

Joey Yi Zhao
Author by

Joey Yi Zhao

Updated on July 19, 2022

Comments

  • Joey Yi Zhao
    Joey Yi Zhao almost 2 years

    React hooks introduces useState for setting component state. But how can I use hooks to replace the callback like below code:

    setState(
      { name: "Michael" },
      () => console.log(this.state)
    );
    

    I want to do something after the state is updated.

    I know I can use useEffect to do the extra things but I have to check the state previous value which requires a bit code. I am looking for a simple solution which can be used with useState hook.

    • MING WU
      MING WU about 5 years
      in class component, I used async and await to achieve the same result like what you did to add a callback in setState. Unfortunately, it is not working in hook. Even if I added async and await , react will not wait for state to update. Maybe useEffect is the only way to do it.
    • Tunn
      Tunn over 2 years
      There's an easy way to do this without useEffect stackoverflow.com/a/70405577/5823517
  • Darryl Young
    Darryl Young about 5 years
    This will fire the console.log on the first render as well as any time counter changes. What if you only want to do something after the state has been updated but not on initial render as the initial value is set? I guess you could check the value in useEffect and decide if you want to do something then. Would that be considered best practice?
  • Admin
    Admin over 4 years
    To avoid running useEffect on initial render, you can create a custom useEffect hook, which doesn't run on initial render. To create such a hook, you can check out this question: stackoverflow.com/questions/53253940/…
  • Mohit Singh
    Mohit Singh about 4 years
    This won't work. You don't know in which order the three statements inside updateAge will actually work. All three are async. The Only thing guaranteed is that first line runs before 3rd (since they work on same state). You don't know anything about 2nd line. This example is too simle to see this.
  • Mohit Singh
    Mohit Singh about 4 years
    setCounter is async you don't know if console.log will be called after setCounter or before.
  • arjun sah
    arjun sah about 4 years
    My friend mohit. I have implemented this technique in a big complex react project when i was moving from react classes to hooks and it works perfectly. Simply try the same logic anywhere in hooks for replacing setState callback and you will know.
  • Mohit Singh
    Mohit Singh about 4 years
    "works in my project isn't an explanation" , read the docs. They are not synchronous at all. You can't say for sure that the three lines in updateAge would work in that order. If it was in sync then whats need of flag, directly call console.log() line after setAge.
  • Dr.Flink
    Dr.Flink about 4 years
    useRef is a much better solution for "ageFlag".
  • Dmitry Lobov
    Dmitry Lobov about 4 years
    And what about case, when i want to call different callbacks in different setState calls, which will change same state value? You answer is wrong, and in shouldn't be marked as correct to not confusing newbies. The true is that setState callbacks its one of the hardest problems while migrating on hooks from classes which hasn't one clear solving method. Sometimes you really wil be enougth some value-depending effect, and sometimes it'll requires some hacky methods, like saving some kind of flags in Refs.
  • ValYouW
    ValYouW almost 4 years
    Thanks! cool approach. Created a sample codesandbox
  • MJ Studio
    MJ Studio almost 4 years
    @ValYouW Thank your for creating sample. The only matter is that if passed arrow function references variable outer function, then it will capture current values not values after state is updated.
  • ValYouW
    ValYouW almost 4 years
    Yes, that's the catchy part with functional components...
  • ford04
    ford04 almost 4 years
    Alternative: useState can be implemented to receive a callback like setState in classes.
  • Ken Ingram
    Ken Ingram almost 4 years
    Apparently, today, I could call setCounter(value, callback) so that some task is executed after state is updated, avoiding a useEffect. I'm not entirely sure if this is a preference or a particular benefit.
  • Ankan-Zerob
    Ankan-Zerob over 3 years
    How to get the previous state value here?
  • MJ Studio
    MJ Studio over 3 years
    @Ankan-Zerob I think, in the Usage part, the state is referring the previous one yet when callback is called. Isn't it?
  • Greg Sadetsky
    Greg Sadetsky over 3 years
    @KenIngram I just tried that and got this very specific error: Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
  • Ken Ingram
    Ken Ingram over 3 years
    Ah. Well. It seemed simple.
  • tonitone120
    tonitone120 over 3 years
    @BimalGrg Does this really work? I cannot replicate this. It fails to compile with this error: expected as assignment or function call and instead saw an expression
  • Bimal Grg
    Bimal Grg over 3 years
    @tonitone120 there is an arrow function inside setCount.
  • tonitone120
    tonitone120 over 3 years
    @BimalGrg The question is asking for the ability to execute code immediately after state is updated. Does this code really help with that task?
  • vsync
    vsync over 3 years
    useEffect cannot be used in all scenarios. sometimes the state gets updated from several places, but you wish to use a called only from one of those places. How can you differentiate easily? A callback is perfect in such situations. A hack would be to use another ugly useState only to be used so a specific change could be detected. very ugly...
  • Aashiq
    Aashiq over 3 years
    Yes it will work,as like in this.setState in class components,
  • Bimal Grg
    Bimal Grg over 3 years
    @tonitone120 if you want execute code immediately after state is updated then you have to use useEffect hook. In useEffect you can check whether state updated or not and perform any action accordingly.
  • Andrew
    Andrew over 3 years
    @MJStudio Hi MJ, thank you for your answer, it is mostly correct, but I'm unable to refer to a specific key in your example. Just try setState({name: 'Bob'}, (n) => {console.log(n.name)}). To be more specific I'll get an error here n.name, how it could be fixed?
  • MJ Studio
    MJ Studio over 3 years
    @Andrew Is it TypeScript error? In my case, const [s, setS] = useStateCallback({ name: 'mj' }); useEffect(() => { setS({ name: 'Bob' }, (n) => { console.log(n.name); }); }, [setS]); work well as print Bob not mj but TypeScript invoke a error that n can be undefined
  • Zsolt Meszaros
    Zsolt Meszaros over 3 years
    prevName won't be "Michael" as useState is async just like setState in class components. You can't update the state on one line and assume it's already changed on the next one. You'll likely use the unchanged state.
  • james h
    james h over 3 years
    OK, thanks I just test this code, you are correct,
  • james h
    james h over 3 years
    It's weird, prevName is Michael, but if u call another function in the callback that use the Name, it's still not updated.
  • Carlos Martínez
    Carlos Martínez over 3 years
    this help me a lot, thanks. I would like to add something because it was giving me failures at first, the order of the useEffect functions is very important. note that you have to write first the useEffect with dependencies and later the "componentDidMount" with no dependecies. that is, as it is in the example. Thanks again.
  • adir abargil
    adir abargil over 3 years
    doesnt it throws error when not callback is provided?
  • Aaron
    Aaron about 3 years
    setCount is asynchronous, right? If so, there would be a race condition, and console.log might print an old value.
  • Kong
    Kong about 3 years
    Agree with @vsync , the second callback should be re-introduce back in useState hook. It feels like a stepback from this.setState from class component. :/
  • Johnny Tisdale
    Johnny Tisdale almost 3 years
    This doesn't seem to work if you need to use the new state value in the callback. For example, if you change your callback to () => console.log('the new value of isVisible = ' + isVisible) it will display the old value.
  • sugaith
    sugaith almost 3 years
    Are you sure? Because the callback is only called when the state has indeed changed.
  • Johnny Tisdale
    Johnny Tisdale almost 3 years
    Yes at least in my scenario. I should mention that I am using React within the Ionic framework, so it is possible the behavior is slightly different because of that.
  • sugaith
    sugaith almost 3 years
    Does the above @Geoman's answer has the same effect in your Ionic environment? Should display the same effect as this answer here. Pretty much the same if you remove the typescript part and other minor differences.
  • sugaith
    sugaith almost 3 years
    By the way: this was tested in both react-native and standard react.js
  • Robert Rendell
    Robert Rendell almost 3 years
    This is the correct approach IMHO. The other examples don't take into account that you might have different callbacks in your code depending on where you're calling setState - sometimes you might want one callback, sometimes another, sometimes none.
  • Abhishek Choudhary
    Abhishek Choudhary over 2 years
    This is not a good alternative to what can be achieved with a callback, we only need callback on a particular setState call and not on every state change, and there is no way to identify that using this.
  • Jochen Shi
    Jochen Shi over 2 years
    const [state, setState] = useStateCallback(1);setState(2, (n) => {console.log(n, state) });why in above use: n is 2 but state is still 1, and there is anyway that state can also print 2?
  • Design by Adrian
    Design by Adrian over 2 years
    It's not guaranteed that doSomethingWCounter is called when setState is finished.
  • Drazen Bjelovuk
    Drazen Bjelovuk over 2 years
    Why can't I find this pattern documented anywhere?
  • krupesh Anadkat
    krupesh Anadkat about 2 years
    This isn't working anymore in 2022
  • krupesh Anadkat
    krupesh Anadkat about 2 years
    this doesn't work.
  • Emile Bergeron
    Emile Bergeron about 2 years
    @DrazenBjelovuk it's documented in the official Hook API doc under the Functional Update section of useState.
  • Can Rau
    Can Rau about 2 years
    In this case that surely works, but if you want to trigger something right after changing the state, but only after actually asynchronously changed then you need useEffect or one of the custom hooks which use useEffect under the hood. Wanted mention this, as it might be confusing to others
  • Azarro
    Azarro about 2 years
    Curious if anyone has come up with a good alternative to the callback for scenarios where you only need one setState call to trigger some side effect. For now, I've been relying on adding a callbackRef, storing my callback in it for the associated state, and in useEffect, I check if the callbackRef has something, execute it, then set it back to null. It just feels very ugly to be doing it this way. I could wrap it up into a custom hook like others seem to have done similarly, but it still seems very annoying and brittle. Just wondering if there's a better react way.
  • famfamfam
    famfamfam about 2 years
    he want to split to another variable that can make you do the next step without wait the setState callback
  • famfamfam
    famfamfam almost 2 years
    not working to me
  • famfamfam
    famfamfam almost 2 years
    not working to me, function still run before state changed