1. ホーム
  2. javascript

[解決済み] react-hooksによるステート更新時の非同期コードの実行

2022-05-17 15:03:13

質問

以下のようなものがあります。

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

...

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

状態を設定するのはやはり非同期なので、これを待つにはどうしたらいいのか setLoading() の呼び出しが終了するのを待つのに最適な方法は何でしょうか?

setLoading() のようなコールバックは受け付けないようです。 setState() のようなコールバックは受け付けないようです。

一例

クラスベース

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

関数型に変換する

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

上記では、各setWhateverの呼び出しを順次実行したいと思います。これは、この動作を再現するために、多くの異なる useEffect フックを設定する必要があるということでしょうか。

どのように解決するのですか?

useState setStateがReactクラスのコンポーネントで行うような、状態の更新が行われた後のコールバックは提供されません。同じ挙動を再現するために、以下のような類似のパターンを利用することができます。 componentDidUpdate という React クラスコンポーネントのライフサイクルメソッドを利用することができます。 useEffect Hooksを使用する

useEffect フックは、レンダリングサイクルが完了した後に、React が変更を監視する必要がある値の配列として、2 番目のパラメータを受け取ります。

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

...

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

EDIT

とは異なり setState のアップデータは useState フックにはコールバックがありませんが、常に useEffect を使えば上記の挙動を再現できます。ただし、読み込みの変化を判断する必要があります

あなたのコードに対する機能的なアプローチは次のようになります。

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