如何使用' setState '回调反应钩子

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

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

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

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

308597 次浏览

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

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

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




setCount(previousCount => previousCount + 1);

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

你的问题很有道理。让我告诉您,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代码只会在特定的条件下执行。

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

自定义钩子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);

我认为,使用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(状态)将打印1而不是2

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

setState方法是异步的,事实上,它不返回承诺。在我们想要更新或调用一个函数的情况下,函数可以在setState函数中调用callback作为第二个参数。 例如,在上面的例子中,你调用了一个函数作为setState回调函数

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

上面的代码适用于类组件,但对于函数组件,我们不能使用setState方法,因此我们可以利用use effect钩子来实现相同的结果。

显而易见的方法是,你可以使用useEffect,如下所示:

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


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

但这也会在第一次呈现时触发,因此我们可以更改如下代码,检查第一次呈现事件并避免状态呈现。因此,可以通过以下方式实现:

我们可以在这里使用user钩子来标识第一次渲染。

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

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


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


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

我有一个用例,我想在状态设置后创建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];
};

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

在promise中包装setState允许在完成后触发任意动作:

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在使用钩子时是否有批量状态更新功能? < / p >

这个怎么样:

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


你可以使用以下方法,我知道获得更新后的最新状态:

    <李> 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;
});
}
    <李> 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 synticevent处理程序中,setState是一个批量更新过程,因此每次状态更改都会等待并返回一个新状态。
setState()并不总是立即更新组件。它可以批处理或延迟更新。“
https://reactjs.org/docs/react-component.html#setstate < / p >

这是一个有用的链接
React是否保持状态更新的顺序? < / p >

我们可以编写一个名为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

我写了自定义挂钩与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];
}

我不认为用useRef区分挂载与否是一个好方法,不是一个更好的方法来确定useState()在useEffect()中生成的值是否为初始值?

const [val, setVal] = useState(null)


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

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

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


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


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

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

我使用-state-with-callback

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..."
/>
);
};
< p >认为: 反应usestate回调 < / p >

如果你不需要异步更新状态,你可以使用一个ref来保存值而不是useState

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

我认为你需要的是useStateuseCallback:

示例代码

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;

我探索了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)
}

编辑

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

最初的发布

我刚刚弄清楚我们是否可以在这里使用Promise来让setState变成可等待的。

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

主要是临时设置useEffect中要触发的resolve函数

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

传递一个函数怎么样?

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

我有一个非常具体的用例,我需要在dom中呈现一个类,然后设置另一个类。这就是我的解决方案,我发现它相当优雅。

const [value1, setValue1] = useState({value: 'whatever', onValue: false})




useEffect(() => {
setValue1(prev => ({
value: 'whatever',
onValue: !prev.onValue,
}));
}, ['whatever'])


 

useEffect(() => {


// if you want to ensure the render happens before doThing2() then put it in a timeout of 1ms,
setTimeout(doThing2, 1);


// or if you are happy to call it immediately after setting value don't include the timeout
doThing2()




}, [value1.onValue])