如何在React Hooks中使用`setState`回调函数

392

React hooks引入useState来设置组件状态。但是我该如何使用hooks来替换下面的回调函数:

setState(
  { name: "Michael" },
  () => console.log(this.state)
);

状态更新后我想做一些事情。

我知道我可以使用 useEffect 来完成额外的操作,但是我需要检查前一个状态值,这需要写一些代码。我正在寻找一个简单的解决方案,可以与 useState 钩子一起使用。


2
在类组件中,我使用了async和await来实现像你在setState中添加回调函数一样的结果。不幸的是,在hook中它并不起作用。即使我添加了async和await,React也不会等待状态更新。也许useEffect是唯一的解决方式。 - MING WU
有一种简单的方法可以在不使用 useEffect 的情况下完成这个。https://dev59.com/C1QJ5IYBdhLWcg3wVEZ0#70405577 - Tunn
23个回答

416

你需要使用useEffect钩子来实现这个功能。

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

const doSomething = () => {
  setCounter(123);
}

useEffect(() => {
   console.log('Do something after counter has changed', counter);
}, [counter]);

如果您希望在初始渲染期间忽略useEffect回调,则需要相应地修改代码。
import React, { useEffect, useRef } from 'react';

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

const doSomething = () => {
  setCounter(123);
}

useEffect(() => {
  // Return early, if this is the first render:
  if ( !didMount.current ) {
    return didMount.current = true;
  }
  // Paste code to be executed on subsequent renders:
  console.log('Do something after counter has changed', counter);
}, [counter]);

113
这将在第一次渲染时触发 console.log,以及每当 counter 改变时。如果你只想在状态更新后执行某些操作而不是在初始渲染时执行,因为初始值已经设置好了,该怎么办呢?我猜你可以在 useEffect 中检查值,并决定是否要执行该操作。这样做算不算最佳实践? - Darryl Young
74
如果我想在不同的setState调用中调用不同的回调函数来更改相同的状态值,该怎么办?你的答案是错误的,不应标记为正确以避免混淆新手。事实上,setState回调是从没有明确解决方法的类迁移到钩子时最困难的问题之一。有时您真的只需要某些值依赖效果,而有时则需要一些hacky方法,例如在Refs中保存某种标志。 - Dmitry Lobov
8
@KenIngram 我刚试过了,得到了这个非常具体的错误:Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect(). - Greg Sadetsky
24
useEffect 不适用于所有场景。有时状态会从多个地方更新,但您希望只从其中一个地方调用。如何轻松区分?在这种情况下,回调函数是完美的选择。一种hack方法是使用另一个丑陋的 useState,仅用于检测特定更改。非常丑陋... - vsync
6
同意@vsync的观点,第二个回调函数应该重新加入到useState钩子中。它让人感觉像是从类组件的this.setState退回了一步。:/ - Kong
显示剩余7条评论

60

1
@BimalGrg 这真的有效吗?我无法复制这个。它编译失败并显示以下错误:预期为赋值或函数调用,但看到了一个表达式 - tonitone120
@tonitone120 在 setCount 函数内部有一个箭头函数。 - Bimal Grg
48
这个问题询问的是能否在状态更新之后立即执行代码。这段代码真的有助于完成这个任务吗? - tonitone120
1
@tonitone120 如果你想在状态更新后立即执行代码,那么你必须使用 useEffect 钩子。在 useEffect 中,你可以检查状态是否已更新并相应地执行任何操作。 - Bimal Grg
1
@DrazenBjelovuk 它在 useStateFunctional Update 部分 下的官方 Hook API 文档中有记录。 - Emile Bergeron
显示剩余3条评论

53

使用useEffect模仿setState的回调函数,仅在状态更新时触发(而不是初始状态):

const [state, setState] = useState({ name: "Michael" })
const isFirstRender = useRef(true)
useEffect(() => {
  if (isFirstRender.current) {
    isFirstRender.current = false // toggle flag after first render/mounting
    return;
  }
  console.log(state) // do something after state has updated
}, [state])

自定义 Hook useEffectUpdate

function useEffectUpdate(callback) {
  const isFirstRender = useRef(true);
  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false; // toggle flag after first render/mounting
      return;
    }
    callback(); // performing action after state has updated
  }, [callback]);
}

// client usage, given some state dep
const cb = useCallback(() => { console.log(state) }, [state]); // memoize callback
useEffectUpdate(cb);

2
备选方案:useState可以实现与类中的setState一样接收回调。参考链接 - ford04

46

我认为使用useEffect并不是一种直观的方式。

我创建了一个包装器来解决这个问题。在这个自定义钩子中,您可以将回调传递给setState参数而不是useState参数。

我刚刚创建了Typescript版本。所以如果你需要在Javascript中使用它,只需从代码中删除一些类型注释即可。

用法

const [state, setState] = useStateCallback(1);
setState(2, (n) => {
  console.log(n) // 2
});

声明

import { SetStateAction, useCallback, useEffect, useRef, useState } from 'react';

type Callback<T> = (value?: T) => void;
type DispatchWithCallback<T> = (value: T, callback?: Callback<T>) => void;

function useStateCallback<T>(initialState: T | (() => T)): [T, DispatchWithCallback<SetStateAction<T>>] {
  const [state, _setState] = useState(initialState);

  const callbackRef = useRef<Callback<T>>();
  const isFirstCallbackCall = useRef<boolean>(true);

  const setState = useCallback((setStateAction: SetStateAction<T>, callback?: Callback<T>): void => {
    callbackRef.current = callback;
    _setState(setStateAction);
  }, []);

  useEffect(() => {
    if (isFirstCallbackCall.current) {
      isFirstCallbackCall.current = false;
      return;
    }
    callbackRef.current?.(state);
  }, [state]);

  return [state, setState];
}

export default useStateCallback;

缺点

如果传递的箭头函数引用了外部函数的变量,那么它将捕获当前值而不是状态更新后的值。 在上面的使用示例中,console.log(state)将输出1而不是2。


5
谢谢!很棒的方法。创建了一个示例[codesandbox](https://codesandbox.io/s/usestatecallback-forked-kzz2z) - ValYouW
如何在此处获取先前的状态值? - Ankan-Zerob
1
@Ankan-Zerob 我认为,在使用部分,state 是指回调函数被调用时的先前状态。是这样吗? - MJ Studio
const [state, setState] = useStateCallback(1);setState(2, (n) => {console.log(n, state) });为什么上述代码中,n是2但state仍然是1,是否有任何方法可以使state也打印出2? - Jochen Shi
对我来说不起作用。 - famfamfam
显示剩余4条评论

20

我曾遇到同样的问题,使用 useEffect 在我的设置中没有起作用(我从多个子组件的数组更新父组件的状态,并且需要知道哪个组件更新了数据)。

将 setState 包装在一个 Promise 中可以在完成后触发任意操作:

import React, {useState} from 'react'

function App() {
  const [count, setCount] = useState(0)

  function handleClick(){
    Promise.resolve()
      .then(() => { setCount(count => count+1)})
      .then(() => console.log(count))
  }


  return (
    <button onClick= {handleClick}> Increase counter </button>
  )
}

export default App;

以下问题引导我朝着正确的方向: 使用hooks时,React是否批处理状态更新函数?


18
setCount 是异步的,对吗?如果是这样,就有可能出现竞态条件,console.log 可能会打印出旧值。 - RavenMan
2
2022年这个已经不再起作用了。 - krupesh Anadkat

15
我使用TypeScript编写了自定义钩子,如果有人需要的话,请参考以下内容。
import React, { useEffect, useRef, useState } from "react";

export const useStateWithCallback = <T>(initialState: T): [state: T, setState: (updatedState: React.SetStateAction<T>, callback?: (updatedState: T) => void) => void] => {
    const [state, setState] = useState<T>(initialState);
    const callbackRef = useRef<(updated: T) => void>();

    const handleSetState = (updatedState: React.SetStateAction<T>, callback?: (updatedState: T) => void) => {
        callbackRef.current = callback;
        setState(updatedState);
    };

    useEffect(() => {
        if (typeof callbackRef.current === "function") {
            callbackRef.current(state);
            callbackRef.current = undefined;
        }
    }, [state]);

    return [state, handleSetState];
}

3
这是我个人认为正确的方法。其他示例没有考虑到您在代码中调用setState时可能具有不同的回调 - 有时您可能需要一个回调,有时需要另一个回调,有时不需要回调。 - Robert Rendell

10

在更新后获取最新状态的方法如下:

  1. 使用 useEffect
    https://reactjs.org/docs/hooks-reference.html#useeffect
    const [state, setState] = useState({name: "Michael"});
    
    const handleChangeName = () => {
      setState({name: "Jack"});
    }
    
    useEffect(() => {
      console.log(state.name); //"Jack"

      //do something here
    }, [state]);
  1. 函数式更新
    https://reactjs.org/docs/hooks-reference.html#functional-updates
    如果新状态是通过先前的状态计算得出的,您可以将一个函数传递给setState。该函数将接收先前的值,并返回一个更新后的值。
    const [state, setState] = useState({name: "Michael"});

    const handleChangeName = () => {
      setState({name: "Jack"})
      setState(prevState => {
        console.log(prevState.name);//"Jack"

        //do something here

        // return updated state
        return prevState;
      });
    }
  1. useRef
    https://reactjs.org/docs/hooks-reference.html#useref
    返回的ref对象将会持续存在于组件的整个生命周期中。
    const [state, setState] = useState({name: "Michael"});

    const stateRef = useRef(state);
    stateRef.current  = state;
    const handleClick = () => {
      setState({name: "Jack"});

      setTimeout(() => {
        //it refers to old state object
        console.log(state.name);// "Michael";

        //out of syntheticEvent and after batch update
        console.log(stateRef.current.name);//"Jack"

        //do something here
      }, 0);
    }

在React的SyntheticEvent处理程序中,setState是批量更新过程,因此每个状态改变都会等待并返回一个新状态。
"setState()不总是立即更新组件。它可能会批量或延迟更新,直到稍后。"
https://reactjs.org/docs/react-component.html#setstate
以下是一个有用的链接:
React是否保留状态更新顺序?

10

setState()方法将组件状态的更改排队,并告诉 React,需要使用更新后的状态重新渲染此组件及其子组件。

setState方法是异步进行的,事实上它并不返回Promise。因此,在我们想要更新或调用函数的情况下,可以将该函数作为setState函数的第二个参数传入callback。 例如,在上面的例子中,您已经将一个函数作为setState回调调用了。

setState(
  { name: "Michael" },
  () => console.log(this.state)
);

上面的代码对于类组件运行良好,但是在函数式组件中,我们不能使用setState方法。因此,我们可以利用useEffect hook来实现相同的结果。

显而易见的方法是,您可以使用以下useEffect:

const [state, setState] = useState({ name: "Michael" })

useEffect(() => {
  console.log(state) // do something after state has updated
}, [state])

但这会在第一次渲染时触发,因此我们可以将代码更改如下,在其中检查第一次渲染事件并避免状态渲染。 因此,可以按以下方式执行实现:

在此处我们可以使用用户钩子来识别第一次渲染。

useRef Hook允许我们在功能组件中创建可变变量。它对于访问DOM节点/React元素以及存储可变变量而不触发重新渲染非常有用。

const [state, setState] = useState({ name: "Michael" });
const firstTimeRender = useRef(true);

useEffect(() => {
 if (!firstTimeRender.current) {
    console.log(state);
  }
}, [state])

useEffect(() => { 
  firstTimeRender.current = false 
}, [])

1
这对我很有帮助,谢谢。我想补充一些内容,因为一开始它给了我失败,useEffect函数的顺序非常重要。请注意,您必须首先编写具有依赖关系的useEffect,然后再编写没有依赖关系的"componentDidMount"。就像示例中那样。再次感谢。 - Carlos Martínez

2

我有一个使用场景,我想在设置状态后进行一些带参数的 API 调用。我不想将这些参数设置为我的状态,所以我创建了一个自定义钩子,以下是我的解决方案:

import { useState, useCallback, useRef, useEffect } from 'react';
import _isFunction from 'lodash/isFunction';
import _noop from 'lodash/noop';

export const useStateWithCallback = initialState => {
  const [state, setState] = useState(initialState);
  const callbackRef = useRef(_noop);

  const handleStateChange = useCallback((updatedState, callback) => {
    setState(updatedState);
    if (_isFunction(callback)) callbackRef.current = callback;
  }, []);

  useEffect(() => {
    callbackRef.current();
    callbackRef.current = _noop; // to clear the callback after it is executed
  }, [state]);

  return [state, handleStateChange];
};

如果没有提供回调函数,它不会抛出错误吗? - adir abargil

1
我们可以编写一个名为useScheduleNextRenderCallback的钩子函数,它返回一个"schedule"函数。在调用setState后,我们可以调用"schedule"函数,传递我们想要在下一次渲染时运行的回调函数。
import { useCallback, useEffect, useRef } from "react";

type ScheduledCallback = () => void;
export const useScheduleNextRenderCallback = () => {
  const ref = useRef<ScheduledCallback>();

  useEffect(() => {
    if (ref.current !== undefined) {
      ref.current();
      ref.current = undefined;
    }
  });

  const schedule = useCallback((fn: ScheduledCallback) => {
    ref.current = fn;
  }, []);

  return schedule;
};

使用示例:

const App = () => {
  const scheduleNextRenderCallback = useScheduleNextRenderCallback();

  const [state, setState] = useState(0);

  const onClick = useCallback(() => {
    setState(state => state + 1);
    scheduleNextRenderCallback(() => {
      console.log("next render");
    });
  }, []);

  return <button onClick={onClick}>click me to update state</button>;
};

缩小测试案例: https://stackblitz.com/edit/react-ts-rjd9jk


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