ChatGPT解决这个技术问题 Extra ChatGPT

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

React hooks 引入了 useState 来设置组件状态。但是如何使用钩子来替换回调,如下面的代码:

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

我想在状态更新后做点什么。

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

在类组件中,我使用 async 和 await 来实现与在 setState 中添加回调一样的结果。不幸的是,它不能在钩子中工作。即使我添加了 async 和 await ,react 也不会等待状态更新。也许 useEffect 是唯一的方法。
没有 useEffect stackoverflow.com/a/70405577/5823517 有一个简单的方法来做到这一点

K
Kostas Minaidis

您需要使用 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]);

这将在第一次渲染时触发 console.log 以及任何时候 counter 更改。如果您只想在状态更新后做某事,而不是在初始渲染时因为设置了初始值,该怎么办?我想您可以检查 useEffect 中的值,然后决定是否要执行某些操作。这会被认为是最佳实践吗?
那么情况如何,当我想在不同的 setState 调用中调用不同的回调时,这会改变相同的状态值?您的回答是错误的,不应将其标记为正确,以免使新手感到困惑。事实是 setState 回调它是最困难的问题之一,同时从没有明确解决方法的类中迁移钩子。有时你真的会足够一些依赖于值的效果,有时它需要一些 hacky 方法,比如在 Refs 中保存某种标志。
@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().
useEffect 不能在所有场景中使用。有时状态会从多个位置更新,您希望仅从其中一个位置使用调用。如何轻松区分?在这种情况下,回调是完美的。黑客将使用另一个丑陋的 useState 仅用于检测特定更改。十分难看...
同意 @vsync ,第二个回调应该在 useState 钩子中重新引入。感觉就像从类组件的 this.setState 退一步。 :/
f
ford04

useEffectonly firing on state updates 模拟 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);

备选方案:useState can be implemented 在类中接收类似于 setState 的回调。
B
Bimal Grg

如果你想更新以前的状态,那么你可以在钩子中这样做:

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


setCount(previousCount => previousCount + 1);

@BimalGrg 这真的有用吗?我无法复制这个。它无法编译并出现以下错误:expected as assignment or function call and instead saw an expression
@tonitone120 setCount 中有一个箭头函数。
@BimalGrg问题是要求在状态更新后立即执行代码的能力。这段代码真的有助于完成这项任务吗?
是的,它会起作用,就像类组件中的 this.setState 一样,
@tonitone120 如果您想在状态更新后立即执行代码,那么您必须使用 useEffect 挂钩。在 useEffect 中,您可以检查状态是否更新并相应地执行任何操作。
M
MJ Studio

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


谢谢!很酷的方法。创建了一个示例 codesandbox
@ValYouW 感谢您创建示例。唯一的问题是,如果传递的箭头函数引用了变量外部函数,那么它将在状态更新后捕获当前值而不是值。
是的,这是功能组件的吸引人的部分......
如何在这里获取之前的状态值?
@Ankan-Zerob 我认为,在使用部分, state 在调用回调时指的是前一个。不是吗?
H
Hugo Merzisen

我遇到了同样的问题,在我的设置中使用 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;

以下问题使我朝着正确的方向前进:Does React batch state update functions when using hooks?


setCount 是异步的,对吗?如果是这样,就会出现竞争条件,并且 console.log 可能会打印一个旧值。
这在 2022 年不再有效
L
Laura Nutt

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 
}, [])

这对我有很大帮助,谢谢。我想添加一些东西,因为它一开始就让我失败了,useEffect 函数的顺序非常重要。请注意,您必须首先编写具有依赖关系的 useEffect,然后编写没有依赖关系的“componentDidMount”。也就是说,就像在示例中一样。再次感谢。
G
Giorgi_Mdivani

如果有人仍然需要它,我会用 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];
}

这是正确的方法恕我直言。其他示例没有考虑到您的代码中可能有不同的回调,具体取决于您调用 setState 的位置 - 有时您可能需要一个回调,有时是另一个,有时没有。
K
Kaiwen Luo

您可以使用以下我知道的方式在更新后获取最新状态:

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

功能更新 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;
      });
    }

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 synthesisEvent handler 中,setState 是一个批量更新的过程,所以每次状态的变化都会被等待并返回一个新的状态。
"setState() 并不总是立即更新组件,它可能会批量更新或者推迟更新. ",
https://reactjs.org/docs/react-component.html#setstate

这是一个有用的链接
Does React keep the order for state updates?


s
sugaith

在你们所有人的帮助下,我能够实现这个自定义钩子:

非常类似于基于类的 this.setState(state, callback)

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

  const setStateCB = (newState, callback) => {
    callbackRef.current = callback;
    setState(newState);
  };

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

  return [state, setStateCB];
};

这样我们就可以像..

const [isVisible, setIsVisible] = useStateWithCallback(false);

...

setIsVisible(true, () => console.log('callback called now!! =)');

保持冷静和快乐的编码!


如果您需要在回调中使用新的状态值,这似乎不起作用。例如,如果您将回调更改为 () => console.log('the new value of isVisible = ' + isVisible),它将显示旧值。
你确定吗?因为只有在状态确实发生变化时才会调用回调。
是的,至少在我的情况下。我应该提到我在 Ionic 框架中使用 React,因此行为可能因此略有不同。
以上@Geoman 的回答在您的 Ionic 环境中是否具有相同的效果?应该在此处显示与此答案相同的效果。如果您删除打字稿部分和其他细微差别,则几乎相同。
顺便说一句:这在 react-native 和标准 react.js 中都经过了测试
A
Akash Singh

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

不提供回调时不会抛出错误吗?
a
arjun sah

您的问题非常有效。让我告诉您 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 OR IF 块,以便在满足这些条件时仅执行该代码块。无论何时效果都会随着依赖数组的变化而运行,但效果内部的 IF 代码将仅在特定条件下执行。


这行不通。您不知道 updateAge 中的三个语句实际上按什么顺序工作。这三个都是异步的。唯一可以保证的是第一行在第三行之前运行(因为它们在相同的状态下工作)。你对第二行一无所知。这个例子太简单了,看不到这一点。
我的朋友莫希特。当我从反应类转移到钩子时,我已经在一个大型复杂的反应项目中实现了这种技术,它工作得很好。只需在钩子中的任何地方尝试相同的逻辑来替换 setState 回调,您就会知道。
“在我的项目中工作不是解释”,阅读文档。它们根本不同步。您不能确定 updateAge 中的三行将按该顺序工作。如果它是同步的,那么需要什么标志,在 setAge 之后直接调用 console.log() 行。
useRef 是“ageFlag”的更好解决方案。
O
Oliver Joseph Ash

我们可以编写一个名为 useScheduleNextRenderCallback 的钩子,它返回一个“调度”函数。在我们调用 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


H
Hamayun

简单的解决方案,只需安装

npm 我使用带有回调的状态

import React from 'react';
import { useStateWithCallbackLazy } from "use-state-with-callback";

const initialFilters = {
  smart_filter: "",
};

const MyCallBackComp = () => {
  const [filters, setFilters] = useStateWithCallbackLazy(initialFilters);

  const filterSearchHandle = (e) => {
    setFilters(
      {
        ...filters,
        smart_filter: e,
      },
      (value) => console.log("smartFilters:>", value)
    );
  };

  return (
    <Input
      type="text"
      onChange={(e) => filterSearchHandle(e.target.value)}
      name="filter"
      placeholder="Search any thing..."
    />
  );
};

记入:REACT USESTATE CALLBACK


S
SiroSong

我不认为用 useRef 区分已安装与否是一个好方法,通过确定 useEffect() 中生成的值 useState() 是否是初始值不是更好的方法吗?

const [val, setVal] = useState(null)

useEffect(() => {
  if (val === null) return
  console.log('not mounted, val updated', val)
}, [val])

T
TylerC

在我们对 setState 回调有原生内置支持之前,我们可以使用普通的 javascript 方式……调用函数并将新变量直接传递给它。

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

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

  function doSomethingWCounter(newCounter) {
    console.log(newCounter); // 123
  }

不能保证在 setState 完成时调用 doSomethingWCounter。
B
Brian Jonathan Meier

如果您不需要异步更新状态,您可以使用 ref 代替 useState 来保存值。

const name = useRef("John");
name.current = "Michael";
console.log(name.current); // will print "Michael" since updating the ref is not async

J
Jar

我探索了 use-state-with-callback npm 库和其他类似的自定义钩子,但最后我意识到我可以这样做:

const [user, setUser] = React.useState(
  {firstName: 'joe', lastName: 'schmo'}
)

const handleFirstNameChange=(val)=> {
  const updatedUser = {
     ...user,
     firstName: val
  }
  setUser(updatedUser)
  updateDatabase(updatedUser)
}

在这种情况下,这肯定有效,但是如果您想在更改状态后立即触发某些事情,但只有在实际异步更改之后,您需要 useEffect 或在后台使用 useEffect 的自定义钩子之一。想提这个,因为它可能会让其他人感到困惑
J
Johnny

已编辑

这里使用promise似乎仍然会在重新渲染后推迟执行,触发两次setState可能是获得最新状态的最佳解决方案。因为 setState 将被列出,我们只需要在重新渲染之前使用 prevState

原帖

我只是想知道我们是否可以在这里使用 Promise 来让 setState 变为可等待状态。

这是我的实验结果,感觉比使用回调更好

主要是在 useEffect 中临时触发解析函数

function useAsyncState(initialState) {
  const [state, setState] = useState(initialState)
  const resolveCb = useRef()

  const handleSetState = (updatedState) => new Promise((resolve, reject) => {
    // force previous promise resolved
    if (typeof resolveCb.current === 'function') {
      resolveCb.current(updatedState)
    }
    resolveCb.current = resolve
    try {
      setState(updatedState)
    } catch(err) {
      resolveCb.current = undefined
      reject(err)
    }
  })

  useEffect(() => {
    if (typeof resolveCb.current === 'function') {
      resolveCb.current(state)
      resolveCb.current = undefined
    }
  }, [state])

  return [state, handleSetState]
}

在组件中使用

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

  const increment = useMemoizedFn(async () => {
    const newCount = await setCount(count + 1)
    console.log(newCount)
  })

  console.log('rerender')

  return (
    <div>
      <h3 onClick={increment}>Hi, {count}</h3>
    </div>
  )
}

R
Ricola

传递一个函数呢?

const [name, setName] = useState(initialName); 
...
setName(() => {
    const nextName = "Michael";
    console.log(nextName);
    return nextName;
  });

j
james h

这个怎么样:

const [Name, setName] = useState("");
...
onClick={()=>{
setName("Michael")
setName(prevName=>{...}) //prevName is Michael?
}}


prevName 不会是“Michael”,因为 useState 与类组件中的 setState 一样是异步的。您不能在一行更新状态并假设它已经在下一行更改。您可能会使用未更改的状态。
好的,谢谢我刚刚测试了这段代码,你是对的,
很奇怪,prevName 是 Michael,但是如果你在回调中调用另一个使用 Name 的函数,它仍然没有更新。
J
José Pereira

我认为您需要的是 useState 和 useCallback:

useState 反应文档;

useCallback 反应文档;

示例代码

import React, { useCallback, useState } from 'react';

const Test = () => {
  const [name, setName] = useState("");
  const testCallback = useCallback(() => console.log(name), [name]);

  return (
    <button onClick={() => {
      setName("Michael")
      testCallback();
    }}>Name</button>
  )
};

export default Test;

K
Kyle Laster

UseEffect 是主要的解决方案。但是正如 Darryl 提到的,使用 useEffect 并传入 state 作为第二个参数有一个缺陷,组件将在初始化过程中运行。如果您只想让回调函数使用更新后的状态值运行,您可以设置一个本地常量并在 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);
}

setCounter 是异步的,您不知道 console.log 是在 setCounter 之后还是之前调用。
他想拆分到另一个变量,让您无需等待 setState 回调即可执行下一步