如何在React钩子上使用`setState`回调


133

React钩子介绍了useState用于设置组件状态的方法。但是我如何使用钩子来代替如下代码的回调:

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

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

我知道我可以useEffect用来做其他事情,但是我必须检查状态以前的值,这需要一个位代码。我正在寻找可以与useState钩子一起使用的简单解决方案。


1
在类组件中,我使用了async和await来实现相同的结果,就像您在setState中添加回调一样。不幸的是,它无法正常运行。即使我添加了async和await,react也不会等待状态更新。也许useEffect是唯一的方法。
MING WU

2
@Zhao,您尚未标记正确答案。能否请您
节省

Answers:


147

您需要使用useEffect钩子来实现。

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

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

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

55
这将console.log在第一个渲染以及任何时间counter更改上触发。如果您只想在状态更新后执行某些操作,而不是在设置初始值时进行初始渲染,该怎么办?我想您可以检查一下值,useEffect然后决定是否要做某事。那将被认为是最佳实践吗?
Darryl Young

2
为了避免useEffect在初始渲染上运行,您可以创建一个自定义useEffect钩子,该钩子不能在初始渲染上运行。要创建这样的钩子,您可以查看以下问题:stackoverflow.com/questions/53253940/…–

14
那么,当我想在不同的setState调用中调用不同的回调时,会改变相同的状态值呢?您的回答是错误的,不应将in标记为正确,以免混淆新手。的确,setState回调其最困难的问题之一,同时又从没有一种明确的解决方法的类中的钩子上进行迁移。有时,您确实会受到一些依赖于价值的影响,有时,它需要一些骇人听闻的方法,例如在Refs中保存某种标志。
德米特里·洛博夫

显然,今天,我可以调用setCounter(value,callback),以便在状态更新后执行某些任务,从而避免了useEffect。我不确定这是偏好还是特别的好处。
肯·英格拉姆

2
@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

25

如果要更新以前的状态,则可以在钩子中执行以下操作:

const [count, setCount] = useState(0);


setCount(previousCount => previousCount + 1);

2
我想按setCounter您的意思setCount
Mehdi Dehghani

@BimalGrg这真的有用吗?我无法复制。它无法通过以下错误进行编译:expected as assignment or function call and instead saw an expression
tonitone120 '20

@ tonitone120在setCount中有一个箭头函数。
Bimal Grg

@BimalGrg问题要求状态更新立即执行代码。这段代码真的对完成这项任务有帮助吗?
tonitone120

是的,它将起作用,就像类组件中的this.setState一样,
Aashiq ahmed 20/12/12

19

useEffect,仅在状态更新时触发:

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

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

useEffect(() => { 
  isFirstRender.current = false // toggle flag after first render/mounting
}, [])

上面将setState最好地模拟回调,并且不会为初始状态触发。您可以从中提取自定义挂钩:

function useEffectUpdate(effect, deps) {
  const isFirstRender = useRef(true) 

  useEffect(() => {
    if (!isFirstRender.current) {
      effect()
    }  
  }, deps) // eslint-disable-line react-hooks/exhaustive-deps

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

// ... Usage inside component
useEffectUpdate(() => { console.log(state) }, [state])

替代方案:useState 可以实现为接收setState类中的回调。
福特

17

我认为,使用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。


1
谢谢!很酷的方法。创建了一个示例代码框
ValYouW

@ValYouW感谢您创建示例。唯一的问题是,如果传递的箭头函数引用变量外部函数,那么它将在状态更新后捕获当前值而不是值。
MJ Studio

是的,那是功能组件的醒目部分……
ValYouW

如何在此获取先前的状态值?
安坎-泽罗布

@ Ankan-Zerob我认为在“用法”部分中,state当调用回调时,它引用的是上一个。是不是
MJ Studio

2

setState() 将组件状态更改入队,并告知React该组件及其子级需要使用更新后的状态重新呈现。

setState方法是异步的,事实上,它不返回promise。因此,在我们要更新或调用函数的情况下,可以在setState函数中将该函数作为第二个参数调用回调。例如,在上述情况下,您已将函数调用为setState回调。

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

上面的代码对于类组件很好用,但是在功能组件的情况下,我们不能使用setState方法,并且可以利用使用效果钩子来达到相同的结果。

显而易见的方法是,ypu可以与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并没有解决问题(我正在从多个子组件的数组更新父状态,并且我需要知道哪个组件更新了数据)。

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

以下问题使我朝着正确的方向: 使用钩子时,React批状态更新功能是否起作用?


0

您的问题非常有效,让我告诉您useEffect默认情况下运行一次,并且在每次依赖项数组更改后运行一次。

检查以下示例::

import React,{ useEffect, useState } from "react";

const App = () => {
  const [age, setAge] = useState(0);
  const [ageFlag, setAgeFlag] = useState(false);

  const updateAge = ()=>{
    setAgeFlag(false);
    setAge(age+1);
    setAgeFlag(true);
  };

  useEffect(() => {
    if(!ageFlag){
      console.log('effect called without change - by default');
    }
    else{
      console.log('effect called with change ');
    }
  }, [ageFlag,age]);

  return (
    <form>
      <h2>hooks demo effect.....</h2>
      {age}
      <button onClick={updateAge}>Text</button>
    </form>
  );
}

export default App;

如果要使用挂钩执行setState回调,请使用标志变量,并在useEffect内提供IF ELSE或IF块,以便在满足该条件时仅执行该代码块。无论何时,效果都会随着依赖项数组的更改而运行,但是效果内部的IF代码将仅在特定条件下执行。


4
这行不通。您不知道updateAge中的三个语句实际上将按哪个顺序工作。这三个都是异步的。唯一可以保证的是第一行在第3行之前运行(因为它们在相同状态下工作)。您对第二行一无所知。这个例子太简单了,看不到这个。
Mohit Singh

我的朋友 当我从反应类转移到钩子时,我已经在一个大型复杂的反应项目中实现了该技术,并且效果很好。只需在钩子上的任何地方尝试相同的逻辑来替换setState回调,您就会知道。
arjun sah

4
“阅读我的项目不是我的工作”,请阅读文档。它们根本不是同步的。您不能肯定地说updateAge中的三行将按此顺序工作。如果它是同步的,那么需要标记,直接在setAge之后调用console.log()行。
Mohit Singh

useRef是“ ageFlag”的更好的解决方案。
Dr.Flink

0

我有一个用例,在设置状态后,我想用一些参数进行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];
};

-3

UseEffect是主要解决方案。但是正如Darryl所述,使用useEffect并将状态作为第二个参数传递有一个缺陷,该组件将在初始化过程中运行。如果只希望回调函数使用更新后的状态值运行,则可以设置一个局部常量,并将其用于setState和回调中。

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

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

  // now you can "do something" with updatedNumber and don't have to worry about the async nature of setState!
  console.log(updatedNumber);
}

5
setCounter是异步的,您不知道在setCounter之后还是之前调用console.log。
Mohit Singh
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.