0

I need to use a callback function, immediately after state hook gets updated.

Before hooks I could do this in a very simple fashion, as following:

myFunction= () => {
    this.setState({
        something: "something"
    }, () => {
     // do something with this.state
    })
}

Here is my code:

const EmployeeDetails = () => 
const [state, setState] = useState({
    employee: undefined,
    companyDocuments: undefined,
    personalDocuments: undefined,
});

useEffect(() => {
    const fetchData = async () => {
        axios
            .all([
                await axios.get("someUrl"),
                await axios.get("someUrl"),
                await axios.get("someUrl"),
            ])
            .then(
                axios.spread((...responses) => {
                    setState({
                        employee: responses[0].data,
                        companyDocuments: responses[1].data,
                        personalDocuments: responses[2].data,
                    });
                })
            )
            .catch(errors => {});
    };
    fetchData()
    .then(processMyEmployee); // should be called something like this
}, []);

const processMyEmployee= () => {
  // crash because state.employee is not fetched yet
}

return (
    <div>it works!</div>
);
};

I've seen some other suggestions on stack, most of them imply using custom hooks, but I believe this is a common problem and therefore must be a common solution to solve it.

FMR
  • 173
  • 7
  • 17
  • Does this answer your question? [How to use \`setState\` callback on react hooks](https://stackoverflow.com/questions/56247433/how-to-use-setstate-callback-on-react-hooks) – ford04 May 07 '20 at 09:40
  • Yes and no. I've seen it before. So in this example `const [counter, setCounter] = useState(0); const doSomething = () => { setCounter(123); } useEffect(() => { console.log('Do something after counter has changed', counter); }, [counter]);` The guy sets the value before using this effect. In my example I set it to undefined, fetch it from the API, and only after that I want to call processMyEmployee. The second answer with useRef should solve my issue, but I thought there might be an easier way. – FMR May 07 '20 at 09:44
  • Yeah, the accepted answer will not mimic the `setState` callback correctly. That's why I posted my own answer. You can have a look at it [here](https://stackoverflow.com/a/61612292/5669456). – ford04 May 07 '20 at 09:47

3 Answers3

3

In order to execute a block of code post the state update is complete, you make use of useEffect hook with the state as dependecy and disable its executign on initial render

const initialRender = useRef(true);
useEffect(() => {
   if(!initialRender.current) {
     processMyEmployee()
   } else {
      initialRender.current = false;
   }
}, [state]);

useEffect(() => {
    const fetchData = async () => {
        axios
            .all([
                await axios.get("someUrl"),
                await axios.get("someUrl"),
                await axios.get("someUrl"),
            ])
            .then(
                axios.spread((...responses) => {
                    setState({
                        employee: responses[0].data,
                        companyDocuments: responses[1].data,
                        personalDocuments: responses[2].data,
                    });
                })
            )
            .catch(errors => {});
    };
    fetchData()
}, []);

const processMyEmployee= () => {
  //processing here
}
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
2

You have to listen to state variables that is changed, so try it like this:

    fetchData()
    .then(processMyEmployee); 
}, [employee]); 

if square brackets are empty useEffect will run only once, when component is being created, but if you add this listener, it will run when that variable is changed.

If error persists add logical switch to you function:

fetchData()
.then(resp=>processMyEmployee(resp)); 
}, [employee]);

const processMyEmployee= (resp) => {
    if(resp){
        //* do something
    }
  }
JozeV
  • 616
  • 3
  • 14
  • 27
  • 1
    Hey, thank you for the answer, problem is before that. When processMyEmployee() gets called, I immediately receive " Unhandled Rejection (TypeError): Cannot read property 'forEach' of undefined" – FMR May 07 '20 at 09:38
  • So I got 2 problems now. Putting `fetchData() .then(resp=>processMyEmployee(resp)); }, [state.employee]);` goes into an infinite loop, because at the beginning of the function, I set it's state as undefined, then axios calls the api, changes the state, then because I listen to the state changes [], useEffect runs again. Also using this `const processMyEmployee= (resp) => { if(resp){ // it works } // it doesn't work }` only "it doesn't work" gets printed. – FMR May 07 '20 at 09:52
1

Your code is mostly correct and it's just about the placement.

Can you try below code and let me know if it helps!

const initialState = {
  employee: undefined,
  companyDocuments: undefined,
  personalDocuments: undefined,
};

const EmployeeDetails = () => {
  const [state, setState] = useState(initialState);

  useEffect(() => {
    const fetchData = async () => {
      axios
        .all([await axios.get('someUrl'), await axios.get('someUrl'), await axios.get('someUrl')])
        .then(
          axios.spread((...responses) => {
            setState({
              employee: responses[0].data,
              companyDocuments: responses[1].data,
              personalDocuments: responses[2].data,
            });
          }),
        )
        .catch(errors => {});
    };


    if (state.employee) {
      processMyEmployee();
    } else {
      // This is to ensure that the call doesn't go in infinite loop
      // Currently my suggestion is strictly using `state.employee` but 
      // you should change it based on your requirement
      fetchData();
    }
  }, [state]);

  const processMyEmployee = () => {
    const { employee } = state;
    const employeeInfo = Object.values(employee);
    employeeInfo.forEach(value => {
      // code goes here
    });
  };

  return <div>it works!</div>;
};
Milind Agrawal
  • 2,724
  • 1
  • 13
  • 20
  • Hi, Milind thank you for your answer. I adjusted the code exactly, it re-renders a few times, then I get: `TypeError: state.employee.forEach is not a function` – FMR May 07 '20 at 09:58
  • Can you console the value of `state.employee` and post here if it's not confidential or share the format atleast. – Milind Agrawal May 07 '20 at 10:00
  • In above code, `processMyEmployee` will only get called when you have some data in `employee`. From what you told, it seems that it was called but it crashes may be because the data in it is not an Array. – Milind Agrawal May 07 '20 at 10:01
  • 1
    I hope you are not trying to use `.forEach` for value other than Array. – Milind Agrawal May 07 '20 at 10:02
  • This is the console.log result (just random data): `address: {city: null, county: null, street: null, streetNumber: null, flatBlock: null, …} companyAddressId: 2 dateOfBirth: null firstName: "Matasescu" grossSalary: null hadUnemploymentBenefits: null hasHomeOffice: null holidays: null hoursPerDay: null id: 201 phone: "08827374823" pin: "12928391212312" startDate: "2020-05-07T06:37:00" username: null wasProfesionallyActiveInLastSixMonths: null workEmail: null __proto__: Object` I had to cut some details, since I went beyond the char limit. – FMR May 07 '20 at 10:06
  • 1
    @FMR It looks like an Object format not Array. Can you please use proper iterator and try. – Milind Agrawal May 07 '20 at 10:08
  • Hmm yes Milind, I was actually focusing so much on state, that I was using `state.employee.forEach(prop => {//bla})`, instead of Object.keys. – FMR May 07 '20 at 10:09
  • 1
    Glad to help! I also edited my code with suggestion to loop the object. Cheers – Milind Agrawal May 07 '20 at 10:12