使用React Hooks在状态更新时执行异步代码

143

我有类似这样的东西:

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 钩子来复制此行为?

6个回答

162

useState的setter方法在更新state后不像React类组件中的setState提供回调函数。为了复制相同的行为,您可以使用Hooks中的useEffect与React类组件中的componentDidUpdate生命周期方法类似的模式。

useEffect hook接受第二个参数作为一个值数组,在渲染周期完成后,React需要监视这些值是否有变化。

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

...

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

编辑

setStateuseState hook 的更新器不同,在 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);
  };

2
那么,如果我们有多个需要等待的 setStates,我们需要使用多个 useEffect 钩子,是吗? - Colin Ricardo
1
@Colin,如果您想在不同状态更改时采取不同的操作,则需要这样做。但是,如果您希望在任何这些多个状态更改时执行相同的操作,则可以传递多个参数,例如[isLoading,isUpdated,showError] - Shubham Khatri
是的,在上面的情况下,你会得到 [isLoading, orders] - Shubham Khatri
但是,为了采取不同的操作,我们需要多个useEffect钩子,对吧?此外,我们如何按顺序执行这些操作呢?难道更改会同时触发两个钩子吗? - Colin Ricardo
那个 usePrevious 钩子是一个不错的点睛之笔,我以前从未见过它 - 谢谢分享! - ohsully
显示剩余4条评论

33

等待您的组件重新渲染。

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

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

setLoading(true);

您可以通过类似以下的方法来提高清晰度:

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

6
嗯,所以我需要添加一个 useEffect() 来监控每个状态,并对其做出反应? - Colin Ricardo
1
@Colin,你可以将useEffect视为componentDidMount或componentDidUpdate,具体取决于上面示例中回调函数传递的最后一个参数[loading]。 - SakoBu
4
说实话,我认为这不是一个好的回答;你能否更好地解释一下doSomething到底是做什么的,以及你的使用场景是什么? - Federkun
1
我认为目前这样做是有意义的。这只是一个人为制造的例子,但 doSomething() 从 API 获取数据,并且需要知道 实际 的当前状态,如果这样说有意义的话。 - Colin Ricardo
那么,是的,useEffect 是正确的方式。 - Federkun
显示剩余2条评论

7

创建了一个自定义的useState钩子,它与普通的useState钩子类似,但是这个自定义钩子的状态更新函数接受一个回调函数,在状态更新和组件重新渲染后执行。

Typescript 解决方案

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 解决方案

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

使用方法

const [state, setState] = useCustomState(myInitialValue);
...
setState(myNewValueOrStateUpdaterCallback, () => {
   // Function called after state update and component rerender
})

回调函数在状态已更改为我的情况下仍然被调用。 - famfamfam

4
你可以创建一个异步状态钩子。
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];
};

那么

const [loading, setLoading] = useAsyncState(false)

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


2

将函数传递给setter而不是值!

不要直接给setter赋新值,而是传递一个箭头函数,该函数接受当前状态值并返回新值。

这将强制它链接状态更新,并在完成所有更新后重新呈现组件。

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

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

现在每次调用incrementCount,它都会将计数增加1,并且不再停留在1。

这对我来说真是神奇的解决办法,我有多个异步函数,在一个函数中调用它们(或通过Promise.any()、Promise.allSettled()),通过在setState中提供我的状态,帮我省去了大量的搜索和更新。这应该是迄今为止最好的答案。 - Oussama Boumaad
非常有帮助的回答! - OlatunjiYSO

0

我有一个建议。

你可以使用React Ref来存储状态变量的状态。然后使用React Ref更新状态变量。这将渲染页面刷新,然后在异步函数中使用React Ref。

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
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接