Raul
Raul

Reputation: 3081

React Hooks - Wait for multiple state updates to finish

I have the following states

const [humans, setHumans] = useState([]);
const [animales, setAnimals] = useState([]);

And the following function

const f = () => { 
   console.log(humans);
   console.log(animals);
}

which only has to be executed when the following useEffect finishes updating both states

useEffect(() => {
   setHumans([ ... ]);
   setAnimals([ ... ]);
   f();
}, []);

How can I make sure that f is executed after both asynchronous state updates has finished?

Upvotes: 6

Views: 7796

Answers (3)

cwallenwein
cwallenwein

Reputation: 698

This is not exactly, what you were looking for, but maybe it's good enough:

Instead of waiting for the update, you can just pass the new values to the function explicitly like this:

Change the logging function to this:

const f = (humans, animals) => { 
   console.log(humans);
   console.log(animals);
}

And then simply log the new state independently from the state update:

useEffect(() => {
   const newHumans = [ ... ];
   const newAnimals = [ ... ];
   setHumans(newHumans);
   setAnimals(newAnimals);
   f(newHumans, newAnimals);
}, []);

This improves readability and was often acceptable behavior for me in the past.

Upvotes: 0

Gideon Mulder
Gideon Mulder

Reputation: 354

If you would like to have your state into a Provider and have your fetching logic somewhere else I use this logic, though technically you don't need to divide your code into multiple files.

//Some Provider
const [humans, setHumans] = useState([]);
const [animales, setAnimals] = useState([]);

//Some Component
const {humans} = useHumanStorage();
const {getHumans} = useGetHumans();
const {animals} = useAnimalStorage();
const {getAnimals} = useGetAnimals();

const [isLoaded, setIsLoaded] = useState<boolean>(false);

const f = (humans, animals) => { 
    console.log(humans);
    console.log(animals);
}

useEffect(() => {
    f();
}, [isLoaded]);

useEffect(() => {
    let promises: Promise<any>[] = [];
    promises.push(getAnimals());
    promises.push(getHumans());
    Promise.all(promises).then(() => {
        setIsLoaded(true);
    });
}, []);

Example for the architecture: https://github.com/bespoyasov/frontend-clean-architecture/tree/master/src

You can also do this in one file:

const [isLoaded, setIsLoaded] = useState<boolean>(false);
const [humans, setHumans] = useState([]);
const [animales, setAnimals] = useState([]);

const getAnimals = async () => {
    const result = await yourFetchLogic();
    setAnimals(result);
}

const getHumans = async () => {
    const result = await yourFetchLogic();
    setHumans(result);
}

const f = (humans, animals) => { 
    console.log(humans);
    console.log(animals);
}

useEffect(() => {
    f();
}, [isLoaded]);

useEffect(() => {
    let promises: Promise<any>[] = [];
    promises.push(getAnimals());
    promises.push(getHumans());
    Promise.all(promises).then(() => {
        setIsLoaded(true);
    });
}, []);

Upvotes: 0

Victor Molina
Victor Molina

Reputation: 2641

Try to use a custom hook "useStateWithCallback" and perform the state updates in a callback chain.

useStateWithCallback

import { useState, useRef, useEffect } from "react";

export default function useStateWithCallback(initialState) {
  const [state, setState] = useState(initialState);

  const callbackRef = useRef(null);

  const setStateCallback = (state, callback) => {
    callbackRef.current = callback; // store passed callback to ref
    setState(state);
  };

  useEffect(() => {
    if (callbackRef.current) {
      callbackRef.current(state);
      callbackRef.current = null; // reset callback
    }
  }, [state]);

  return [state, setStateCallback];
}

Code

const [humans, setHumans] = useStateWithCallback([]);
const [animals, setAnimals] = 
useStateWithCallback([]);

const logStates = () => {
   console.log(humans);
   console.log(animals);
}

const updateStates = () => {
   return new Promise((resolve) => {
      setHumans([...], () => {
         setAnimals([...], resolve);
      }
   });
}

useEffect(() => {
   (async () => {
      await updateStates();
      logAnimals();
   })();
}, []);

With this example, you will be able to use the functionality of "waiting for multiple state updates before doing something" outside useEffect too, like this:

const otherFunction = async () => {
    await updateStates();
    doSomething();
}

If you have problems with not up-to-date states, then wrap the method in a useCallback, adding the states to the deps array.

Upvotes: 4

Related Questions