141

I have something like:

const [loading, setLoading] = useState(false);

...

setLoading(true);
doSomething(); // <--- when here, loading is still false. 

Setting state is still async, so what's the best way to wait for this setLoading() call to be finished?

The setLoading() doesn't seem to accept a callback like setState() used to.

an example

class-based

getNextPage = () => {
    // This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
    goToTop();

    if (this.state.pagesSeen.includes(this.state.page + 1)) {
      return this.setState({
        page: this.state.page + 1,
      });
    }

    if (this.state.prefetchedOrders) {
      const allOrders = this.state.orders.concat(this.state.prefetchedOrders);
      return this.setState({
        orders: allOrders,
        page: this.state.page + 1,
        pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
        prefetchedOrders: null,
      });
    }

    this.setState(
      {
        isLoading: true,
      },
      () => {
        getOrders({
          page: this.state.page + 1,
          query: this.state.query,
          held: this.state.holdMode,
          statuses: filterMap[this.state.filterBy],
        })
          .then((o) => {
            const { orders } = o.data;
            const allOrders = this.state.orders.concat(orders);
            this.setState({
              orders: allOrders,
              isLoading: false,
              page: this.state.page + 1,
              pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
              // Just in case we're in the middle of a prefetch.
              prefetchedOrders: null,
            });
          })
          .catch(e => console.error(e.message));
      },
    );
  };

convert to function-based

  const getNextPage = () => {
    // This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
    goToTop();

    if (pagesSeen.includes(page + 1)) {
      return setPage(page + 1);
    }

    if (prefetchedOrders) {
      const allOrders = orders.concat(prefetchedOrders);
      setOrders(allOrders);
      setPage(page + 1);
      setPagesSeen([...pagesSeen, page + 1]);
      setPrefetchedOrders(null);
      return;
    }

    setIsLoading(true);

    getOrders({
      page: page + 1,
      query: localQuery,
      held: localHoldMode,
      statuses: filterMap[filterBy],
    })
      .then((o) => {
        const { orders: fetchedOrders } = o.data;
        const allOrders = orders.concat(fetchedOrders);

        setOrders(allOrders);
        setPage(page + 1);
        setPagesSeen([...pagesSeen, page + 1]);
        setPrefetchedOrders(null);
        setIsLoading(false);
      })
      .catch(e => console.error(e.message));
  };

In the above, we want to run each setWhatever call sequentially. Does this mean we need to set up many different useEffect hooks to replicate this behavior?

GalAbra
  • 5,048
  • 4
  • 23
  • 42
Colin Ricardo
  • 16,488
  • 11
  • 47
  • 80

6 Answers6

162

useState setter doesn't provide a callback after state update is done like setState does in React class components. In order to replicate the same behaviour, you can make use of the a similar pattern like componentDidUpdate lifecycle method in React class components with useEffect using Hooks

useEffect hooks takes the second parameter as an array of values which React needs to monitor for change after the render cycle is complete.

const [loading, setLoading] = useState(false);

...

useEffect(() => {
    doSomething(); // This is be executed when `loading` state changes
}, [loading])
setLoading(true);

EDIT

Unlike setState, the updater for useState hook doesn't have a callback, but you can always use a useEffect to replicate the above behaviour. However you need to determine the loading change

The functional approach to your code would look like

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

const prevLoading = usePrevious(isLoading);

useEffect(() => {
   if (!prevLoading && isLoading) {
       getOrders({
          page: page + 1,
          query: localQuery,
          held: localHoldMode,
          statuses: filterMap[filterBy],
      })
      .then((o) => {
        const { orders: fetchedOrders } = o.data;
        const allOrders = orders.concat(fetchedOrders);

        setOrders(allOrders);
        setPage(page + 1);
        setPagesSeen([...pagesSeen, page + 1]);
        setPrefetchedOrders(null);
        setIsLoading(false);
      })
      .catch(e => console.error(e.message));
   }
}, [isLoading, preFetchedOrders, orders, page, pagesSeen]);

const getNextPage = () => {
    // This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
    goToTop();

    if (pagesSeen.includes(page + 1)) {
      return setPage(page + 1);
    }

    if (prefetchedOrders) {
      const allOrders = orders.concat(prefetchedOrders);
      setOrders(allOrders);
      setPage(page + 1);
      setPagesSeen([...pagesSeen, page + 1]);
      setPrefetchedOrders(null);
      return;
    }

    setIsLoading(true);
  };
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 2
    So, in the case of having multiple setStates that we want to wait for, we need multiple useEffect hooks, right? – Colin Ricardo Feb 20 '19 at 17:34
  • 1
    @Colin, if you want to take different actions when different states change then yes, but if you want to do the sme thing on any of those multiple states change, then you could pass multiple arguments like `[isLoading, isUpdated, showError]` – Shubham Khatri Feb 20 '19 at 17:36
  • yes so in the above case you would have `[isLoading, orders]` – Shubham Khatri Feb 20 '19 at 17:38
  • but to take different actions, we need multiple useEffect hooks, right? Also, how can we do sequential actions this way? Wouldn't a change trigger both hooks at the same time? – Colin Ricardo Feb 20 '19 at 17:46
  • what do you mean by sequential actions. If you update your code with the desired behaviour, I can have a look and suggest a solution if I know it – Shubham Khatri Feb 20 '19 at 17:48
  • Updated the answer – Shubham Khatri Feb 20 '19 at 18:11
  • That `usePrevious` hook is a nice touch, I hadn't seen that before -- thanks for sharing! – ohsully Aug 02 '19 at 21:41
  • BTW, you can use multiple `useEffect` declarations, each one bound to the property you're interested in. – ACV Oct 14 '20 at 11:51
  • After a whole day struggle and hundreds of attempts, finally this fixed my issue – STBox Jan 04 '22 at 11:56
33

Wait until your component re-render.

const [loading, setLoading] = useState(false);

useEffect(() => {
    if (loading) {
        doSomething();
    }
}, [loading]);

setLoading(true);

You can improve clarity with something like:

function doSomething() {
  // your side effects
  // return () => {  }
}

function useEffectIf(condition, fn) {
  useEffect(() => condition && fn(), [condition])
}

function App() {
  const [loading, setLoading] = useState(false);
  useEffectIf(loading, doSomething)

  return (
    <>
      <div>{loading}</div>
      <button onClick={() => setLoading(true)}>Click Me</button>
    </>
  );
}
Federkun
  • 36,084
  • 8
  • 78
  • 90
  • 6
    Hmm, so I need to add a `useEffect()` to monitor each piece of state to react to it? – Colin Ricardo Dec 22 '18 at 20:10
  • 1
    @Colin you can think of useEffect as componentDidMount or componentDidUpdate depending on the last arg passed to the callback in the example above [loading] – SakoBu Dec 22 '18 at 20:13
  • 3
    To be honest, I don't think this is a good answer; can you explain better what `doSomething` really do, and what's your use case? – Federkun Dec 22 '18 at 20:16
  • 1
    I think this makes sense for now. This is a contrived example, but `doSomething()` fetches from API, and needs to know the _actual_ current state, if that makes sense. – Colin Ricardo Dec 22 '18 at 20:18
  • @Colin Good explanation with examples of the hooks API https://egghead.io/lessons/react-use-the-usestate-react-hook – SakoBu Dec 22 '18 at 20:21
  • then yeah, `useEffect` is the way to go – Federkun Dec 22 '18 at 20:35
  • why not calling setLoading in the effect, if `doSomething` fetches from Api the right way could be like this : `useEffect(() => { setLoading(true); doSomething().finally(() => setLoading(false))}, [])`. Note the `[]` is used to executes the effect only ones like `componentDidMount` – Olivier Boissé Dec 22 '18 at 23:27
6

Created a custom useState hook which works similar to the normal useState hook except that the state updater function for this custom hook takes a callback that will be executed after the state is updated and component rerendered.

Typescript Solution

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

type OnUpdateCallback<T> = (s: T) => void;
type SetStateUpdaterCallback<T> = (s: T) => T;
type SetStateAction<T> = (newState: T | SetStateUpdaterCallback<T>, callback?: OnUpdateCallback<T>) => void;

export function useCustomState<T>(init: T): [T, SetStateAction<T>];
export function useCustomState<T = undefined>(init?: T): [T | undefined, SetStateAction<T | undefined>];
export function useCustomState<T>(init: T): [T, SetStateAction<T>] {
    const [state, setState] = useState<T>(init);
    const cbRef = useRef<OnUpdateCallback<T>>();

    const setCustomState: SetStateAction<T> = (newState, callback?): void => {
        cbRef.current = callback;
        setState(newState);
    };

    useEffect(() => {
        if (cbRef.current) {
            cbRef.current(state);
        }
        cbRef.current = undefined;
    }, [state]);

    return [state, setCustomState];
}

Javascript solution

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

export function useCustomState(init) {
    const [state, setState] = useState(init);
    const cbRef = useRef();

    const setCustomState = (newState, callback) => {
        cbRef.current = callback;
        setState(newState);
    };

    useEffect(() => {
        if (cbRef.current) {
            cbRef.current(state);
        }
        cbRef.current = undefined;
    }, [state]);

    return [state, setCustomState];
}

Usage

const [state, setState] = useCustomState(myInitialValue);
...
setState(myNewValueOrStateUpdaterCallback, () => {
   // Function called after state update and component rerender
})
Cels
  • 1,212
  • 18
  • 25
3

you can create a async state hooks

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

  const asyncSetState = value => {
    return new Promise(resolve => {
      setState(value);
      setState((current) => {
        resolve(current);
        return current;
      });
    });
  };

  return [state, asyncSetState];
};

then

const [loading, setLoading] = useAsyncState(false)

const submit = async () => {
  await setLoading(true)
  dosomething() 
}

chen chen
  • 49
  • 3
2

Pass a function to the setter instead of value!

instead of giving a new value to the setter directly, pass it an arrow function that takes the current state value and returns the new value.

it will force it to chain the state updates and after it's done with all of them, it will rerender the component.

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

const incrementCount = () => {
    setCounter( (counter) => { return counter + 1 } )
}

now every time incrementCount is called, it will increase the count by one and it will no longer be stuck at 1.

ARiyou Jahan
  • 622
  • 6
  • 7
  • This magically worked for me, I had multiple async functions that I call on a single one, (or via Promise.any(), or Promise.allSetteled()) and by providing my state on setState just saved me tons of search, and update. this should be the best answer so far – Oussama Boumaad Jul 10 '23 at 22:13
  • Really helpful answer! – OlatunjiYSO Jul 13 '23 at 07:18
0

I have a suggestion for this.

You could possibly use a React Ref to store the state of the state variable. Then update the state variable with the react ref. This will render a page refresh, and then use the React Ref in the async function.

const stateRef = React.useRef().current
const [state,setState] = useState(stateRef);

async function some() {
  stateRef = { some: 'value' }
  setState(stateRef) // Triggers re-render
  
  await some2();
}

async function some2() {
  await someHTTPFunctionCall(stateRef.some)
  stateRef = null;
  setState(stateRef) // Triggers re-render
}

Travis Delly
  • 1,194
  • 2
  • 11
  • 20