使用 react-hooks 更新状态时执行异步代码

Posted

技术标签:

【中文标题】使用 react-hooks 更新状态时执行异步代码【英文标题】:Executing async code on update of state with react-hooks 【发布时间】:2019-05-22 18:05:39 【问题描述】:

我有类似的东西:

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 挂钩来复制这种行为?

【问题讨论】:

【参考方案1】:

useState setter 不会像在 React 类组件中的 setState 那样在状态更新完成后提供回调。为了复制相同的行为,您可以在 React 类组件中使用类似的模式,如 componentDidUpdate 生命周期方法和 useEffect using Hooks

useEffect hooks 将第二个参数作为一个值数组,React 需要在渲染周期完成后监控这些值的变化。

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

...

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

编辑

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

【讨论】:

那么,在我们想要等待多个 setState 的情况下,我们需要多个 useEffect 钩子,对吧? @Colin,如果您想在不同的状态发生变化时采取不同的行动,那么可以,但是如果您想对这些多个状态中的任何一个进行更改,那么您可以传递多个参数,例如 @ 987654331@ 是的,所以在上述情况下,您将拥有[isLoading, orders] 但是要采取不同的行动,我们需要多个 useEffect 挂钩,对吧?另外,我们如何以这种方式执行顺序操作?更改不会同时触发两个钩子吗? 顺序操作是什么意思。如果您使用所需的行为更新您的代码,如果我知道的话,我可以看看并提出解决方案【参考方案2】:

等到您的组件重新渲染。

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

【讨论】:

嗯,所以我需要添加一个useEffect() 来监控每​​个状态以对其做出反应? @Colin 您可以将 useEffect 视为 componentDidMount 或 componentDidUpdate 取决于上例中传递给回调的最后一个参数 [loading] 说实话,我不认为这是一个好的答案;你能更好地解释一下doSomething 的真正作用吗?你的用例是什么? 我认为现在这很有意义。这是一个人为的例子,但doSomething() 从 API 获取,如果有意义的话,需要知道 实际 当前状态。 @Colin 很好的解释,带有 hooks API 的例子egghead.io/lessons/react-use-the-usestate-react-hook【参考方案3】:

创建了一个自定义的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
)

【讨论】:

【参考方案4】:

我对此有一个建议。

您可以使用 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


【讨论】:

以上是关于使用 react-hooks 更新状态时执行异步代码的主要内容,如果未能解决你的问题,请参考以下文章

错误:使用 react-hooks 时超出最大更新深度

在 React-Hooks UseRef 我无法获得新的状态值

调度触发后使用状态

React(五)深入React-Hooks工作机制,原理

区块链 xuperchain 同步模式 纯异步模式 异步阻塞模式 怎么启动

如何使用 react-hooks (useEffect) 缓冲流数据以便能够一次更新另一个组件以避免多次重新渲染?