Javascript – Executing async code on update of state with react-hooks

callbackjavascriptreact-hooksreactjs

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?

Best Answer

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