150

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?

6 Answers 6

166

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);
  };
Sign up to request clarification or add additional context in comments.

1 Comment

BTW, you can use multiple useEffect declarations, each one bound to the property you're interested in.
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>
    </>
  );
}

7 Comments

Hmm, so I need to add a useEffect() to monitor each piece of state to react to it?
@Colin you can think of useEffect as componentDidMount or componentDidUpdate depending on the last arg passed to the callback in the example above [loading]
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?
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 Good explanation with examples of the hooks API egghead.io/lessons/react-use-the-usestate-react-hook
|
7

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
})

1 Comment

callback still called after state changed to me
6

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() 
}

Comments

3

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.

Comments

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
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.