何时使用React的setState回调函数

298

当React组件的状态发生改变时,将调用render方法。因此,对于任何状态更改,都可以在渲染方法体中执行操作。那么,setState回调函数有特定的使用情况吗?


4
目前不清楚您在问什么。您能提供一些代码吗? - Davin Tryon
4
setState 回调函数用于在状态确实已经被改变后执行任何想要做的事情。由于 setState 是异步的,如果你想调用一个函数并确保新状态已经加载,那么就需要使用回调函数。 - Jayce444
4
setState回调函数的使用场景很明确。当你想要在特定状态被更新后运行一个函数时,你会使用它。如果将此函数放在 render() 中,则每次更新任何状态时它都会运行,这可能不是你想要的。这也会使你的代码变得难以阅读和理解。 - M3RS
一个用途是当您使用状态存储来自服务器的结果时...您想要现在就得到它,而不是在渲染之后,因为状态是自定义的,例如...myVar,setMyVar。 - Jonathan Orrego
7个回答

326

是的,有,因为 setState 以一种 异步 的方式工作。这意味着在调用 setState 后,this.state 变量不会立即更改。所以,如果您想在设置状态变量后立即执行操作并返回结果,则回调将非常有用。

考虑下面的例子:

....
changeTitle: function changeTitle (event) {
  this.setState({ title: event.target.value });
  this.validateTitle();
},
validateTitle: function validateTitle () {
  if (this.state.title.length === 0) {
    this.setState({ titleError: "Title can't be blank" });
  }
},
....

上述代码可能无法按预期工作,因为在对其执行验证之前,title变量可能尚未发生变化。现在你可能会想,我们可以在render()函数本身中执行验证,但如果我们可以在changeTitle函数本身中处理这个问题,那么这将使您的代码更有组织性和可读性,这将是更好和更清晰的方式。
在这种情况下,回调函数是有用的。
....
changeTitle: function changeTitle (event) {
  this.setState({ title: event.target.value }, function() {
    this.validateTitle();
  });

},
validateTitle: function validateTitle () {
  if (this.state.title.length === 0) {
    this.setState({ titleError: "Title can't be blank" });
  }
},
....

另一个例子是当你想要在状态改变时 dispatch 一个动作时,你会想要在回调中执行而不是在render()方法中执行,因为每次重新渲染时都会调用该方法,因此可能会出现许多这样的情况,你需要回调函数。 另一种情况是API调用 可能会出现这样一种情况,当特定状态发生更改时需要进行API调用,如果在渲染方法中执行,则会在每次呈现onState更改或因某些传递给Child Component的属性发生更改时被调用。
在这种情况下,您需要使用setState回调将更新后的状态值传递给API调用。
....
changeTitle: function (event) {
  this.setState({ title: event.target.value }, () => this.APICallFunction());
},
APICallFunction: function () {
  // Call API with the updated value
}
....

5
我理解它的性质是异步的。 我的问题是,除了代码可读性(例如更好的代码可读性)之外,是否有什么特定的事情只能使用setState回调来完成,而可能不支持render方法体? - Sahil Jain
@SahilJain 验证是正确的例子,您不希望在render()函数中处理它,因为这样每次您在render()中进行任何更改时都会调用它,您只想在输入更改时调用它,因此应该在函数本身中调用。 - Shubham Khatri
React在渲染期间禁止更改状态。因此,将验证放入回调函数中是正确的做法。 - webdeb
如果(this.title.length === 0){应该是this.state.title.length,对吗? - Dmitry Minkovsky
7
第一个用例可能不是个好主意。setState回调会在重新渲染后触发,所以你没有充分的理由引起双重渲染。这正是函数参数(更新程序)的目的。你只需要运行 setState(state => state.title.length ? { titleError: "Title can't be blank" } : null),然后更改就会堆叠起来。不需要进行双重渲染。 - R Esmond
setState 不是异步的,它是一个闭包。 - Flavio Del Grosso

76
this.setState({
    name:'value' 
},() => {
    console.log(this.state.name);
});

28
谢谢您提供这段代码片段,它可能会提供一些有限的、即时的帮助。通过展示为何这是一个好解决方案,一份适当的解释将极大地提高其长期价值,并且使其对未来读者具有类似问题的更多用途。请[编辑]您的答案以添加一些解释,包括您所做出的假设。 - Machavity
1
当您想在状态改变后调用函数时,可以使用该方法。 - Araz Babayev
如果您想设置多个状态属性,例如名称、姓氏等,该怎么办? - Sumanth Varada
4
不适用于React Hook的useState。 - abitcode

48

我脑海中首先想到的用例是api调用,它不应该进入渲染,因为它会在每个状态改变时运行。 API调用应该仅在特定状态更改时执行,而不是在每次渲染时执行。

changeSearchParams = (params) => {
  this.setState({ params }, this.performSearch)
} 

performSearch = () => {
  API.search(this.state.params, (result) => {
    this.setState({ result })
  });
}

因此,对于任何状态变化,都可以在渲染方法体中执行操作。

这是一种非常不好的做法,因为render方法应该是纯净的,也就是说,不应该执行任何动作、状态变更或API调用,只需组合您的视图并返回它。操作应该仅在某些事件上执行。例如,componentDidMount就是一个事件而不是渲染。


39

考虑setState调用

this.setState({ counter: this.state.counter + 1 })

IDEA

setState可能在异步函数中被调用

因此,您不能依赖于this。如果上述调用是在异步函数内部进行的,则this将引用该组件状态的那一时刻,但我们希望这将引用在调用setState或开始异步任务时状态内的属性。由于任务是异步调用,因此该属性可能已经发生了变化。因此,使用callback函数来引用状态的某个属性是不可靠的,其参数为previousStateprops,这意味着当异步任务完成并且该使用setState调用更新状态时,prevState将引用当前状态,即当setState尚未启动时的状态,确保nextState不会被损坏。

错误代码:会导致数据损坏

this.setState(
   {counter:this.state.counter+1}
 );

使用带有回调函数的setState来纠正代码:

 this.setState(
       (prevState,props)=>{
           return {counter:prevState.counter+1};
        }
    );

因此,每当我们需要根据刚刚拥有的属性值将当前状态更新为下一个状态,并且所有这些都是在异步方式下进行的,使用setState作为回调函数是一个好主意。
我在这里尝试用codepen进行了解释 CODE PEN

4
有时我们需要一个代码块,在setState之后执行某些操作,以确保状态已被更新。这就是setState回调函数发挥作用的地方。
例如,有一种情况,我需要为20个客户中的2个客户启用模态框,在我们启用它的客户中,有一系列耗时的API调用,因此它看起来像这样:
async componentDidMount() {
  const appConfig = getCustomerConfig();
  this.setState({enableModal: appConfig?.enableFeatures?.paymentModal }, async 
   ()=>{
     if(this.state.enableModal){
       //make some API call for data needed in poput
     }
  });
}

在渲染函数中,UI块需要 enableModal 布尔值,这就是为什么我在这里使用了 setState ,否则只需检查一次条件,然后调用API设置或不设置即可。


2

函数组件的setState回调

所有的答案都是关于如何将一个回调函数传递给类组件的setState函数。但对于一个函数组件,我强烈建议使用自定义的钩子函数:

import { useRef, useCallback, useEffect, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import isFunction from 'lodash.isfunction';

type StateFunctionType<S> = Dispatch<SetStateAction<S>>;
export type SetStateCallbackGeneric<S> = (
  x: S | StateFunctionType<S>,
  cb?: (newState: S) => void
) => void;

const useStateCallback = <T>(
  initialState: T
): [T, SetStateCallbackGeneric<T>] => {
  const [state, setState] = useState<T>(initialState);
  const cbRef = useRef<any>(null);

  const setStateCallback: SetStateCallbackGeneric<T> = useCallback(
    (newState, cb) => {
      cbRef.current = cb;
      setState(newState as any);
    },
    []
  );

  useEffect(() => {
    if (isFunction(cbRef?.current)) {
      // @ts-ignore
      cbRef?.current?.(state);
      cbRef.current = null;
    }
  }, [state]);

  return [state, setStateCallback];
};

export default useStateCallback;

通过使用这个钩子函数,你可以轻松地将一个回调函数传递给设置函数,请看下面的例子:
const [text, setText] = useStateCallback<string>('')

const handleFoo = (txt: string) => {
  setText(txt, () => {
    // Do What you want exactly AFTER text gets updated
  })
};

0

setState 回调函数有一个罕见但重要的用例。

当您更新属性 x 后想要重新计算状态,并且您的重新计算可能会再次更新 x。您不能仅仅依赖于 xuseEffect,因为 React 会将其视为“检测到循环依赖”。您可以通过使用 setState 回调函数来规避 React。但请确保您不会创建无限循环。

我已经在两个大型 React 项目中应用了这个解决方案。


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