Reputation: 42500
React hooks introduces useState
for setting component state. But how can I use hooks to replace the callback like below code:
setState(
{ name: "Michael" },
() => console.log(this.state)
);
I want to do something after the state is updated.
I know I can use useEffect
to do the extra things but I have to check the state previous value which requires a bit code. I am looking for a simple solution which can be used with useState
hook.
Upvotes: 420
Views: 420105
Reputation: 163
Using flushSync
If the reason you want to do something after setState is to somehow interact with the dom I suggest using the flushSync
callback provided by ReactDOM.
import { flushSync } from 'react-dom';
flushSync(() => {
setShowButton(true)
});
buttonRef.current?.focus()
Docs: https://react.dev/reference/react-dom/flushSync
flushSync
lets you force React to flush any updates inside the provided callback synchronously. This ensures that the DOM is updated immediately.
This can hurt performance of your app so only use it when it's really needed. Valid reasons to use it would for example be
setState
call makes visibleUpvotes: 0
Reputation: 1
Custom Hook for useState
with Callback:
import { useCallback, useEffect, useRef, useState } from 'react';
// Define a generic function type for the updater and the callback
type Updater<T> = T | ((prevState: T) => T);
type Callback<T> = (state: T) => void;
function useStateCallback<T>(initialState: T): [T, (stateUpdater: Updater<T>, cb?: Callback<T>) => void] {
const [state, setState] = useState<T>(initialState);
const cbRef = useRef<Callback<T> | undefined>(undefined); // Ref to hold the callback
const setStateCallback = useCallback(
(stateUpdater: Updater<T>, cb?: Callback<T>) => {
cbRef.current = cb; // Store the callback in ref
// Set the state, handle function type updater for prevState
setState(prevState => typeof stateUpdater === 'function'
? (stateUpdater as (prevState: T) => T)(prevState)
: stateUpdater);
},
[]
);
// useEffect to call the callback after state update
useEffect(() => {
if (cbRef.current) {
cbRef.current(state); // Call the callback with the updated state
cbRef.current = undefined; // Reset the callback ref
}
}, [state]);
return [state, setStateCallback];
}
export default useStateCallback;
This hook adds a bit more complexity, but effectively mimics class component setState
in hooks.
Potential Usage:
import React from 'react';
import useStateCallback from './useStateCallback';
const ExampleComponent: React.FC = () => {
const [count, setCount] = useStateCallback(0);
// Example usage of setState with a callback
const incrementAndLog = () => {
setCount(count + 1, (newCount) => {
console.log(`Count updated to: ${newCount}`);
});
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementAndLog}>Increment</button>
</div>
);
};
export default ExampleComponent;
Upvotes: 0
Reputation: 8146
If you want to update previous state then you can do like this in hooks:
const [count, setCount] = useState(0);
setCount(previousCount => previousCount + 1);
Docs:
Upvotes: 60
Reputation: 3806
I have a very specific use case where I needed to render a class in the dom, then set another class. This was my solution which I found to be quite elegant.
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])
Upvotes: 1
Reputation: 2922
What about passing a function?
const [name, setName] = useState(initialName);
...
setName(() => {
const nextName = "Michael";
console.log(nextName);
return nextName;
});
Upvotes: -1
Reputation: 36
Using promise here seems still postpone the execution after rerender, triggering setState
twice may be the best solution to get the latest state. Because the setState will be listed and we just need to get prevState
to use before rerendering.
I just figured out if we can use a Promise
here to let setState become awaitable.
Here is my experiment result, feels better then using a callback
Mainly temp the resolve function to trigger in 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]
}
using in component
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>
)
}
Upvotes: 1
Reputation: 22885
You need to use useEffect
hook to achieve this.
const [counter, setCounter] = useState(0);
const doSomething = () => {
setCounter(123);
}
useEffect(() => {
console.log('Do something after counter has changed', counter);
}, [counter]);
If you want the useEffect
callback to be ignored during the first initial render, then modify the code accordingly:
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]);
Upvotes: 431
Reputation: 2010
I explored the use-state-with-callback npm library, and other similar custom hooks, but in the end I realized I can just do something like this:
const [user, setUser] = React.useState(
{firstName: 'joe', lastName: 'schmo'}
)
const handleFirstNameChange=(val)=> {
const updatedUser = {
...user,
firstName: val
}
setUser(updatedUser)
updateDatabase(updatedUser)
}
Upvotes: 0
Reputation: 1
If you don't need to update state asynchronously you can use a ref to save the value instead of useState
.
const name = useRef("John");
name.current = "Michael";
console.log(name.current); // will print "Michael" since updating the ref is not async
Upvotes: 0
Reputation: 29
Simple solution, Just install
npm i use-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..."
/>
);
};
credited to: REACT USESTATE CALLBACK
Upvotes: 2
Reputation: 115
Until we have native built in support for setState callback, we can do the plain javascript way ... call the function and pass the new variables to it directly.
const [counter, setCounter] = useState(0);
const doSomething = () => {
const newCounter = 123
setCounter(newCounter);
doSomethingWCounter(newCounter);
};
function doSomethingWCounter(newCounter) {
console.log(newCounter); // 123
}
Upvotes: 0
Reputation: 1
I don't think that distinguish mounted or not with useRef is a good way, isn't a better way by determining the value genetated useState() in useEffect() whether it is the initial value?
const [val, setVal] = useState(null)
useEffect(() => {
if (val === null) return
console.log('not mounted, val updated', val)
}, [val])
Upvotes: 0
Reputation: 74500
Mimic setState
callback with useEffect
, only firing on state updates (not initial state):
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);
Upvotes: 54
Reputation: 383
I wrote custom hook with typescript if anyone still needs it.
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];
}
Upvotes: 15
Reputation: 2741
We can write a hook called useScheduleNextRenderCallback
that returns a "schedule" function. After we call setState
, we can call the "schedule" function, passing a callback that we want to run on the next render.
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;
};
Example usage:
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>;
};
Reduced test case: https://stackblitz.com/edit/react-ts-rjd9jk
Upvotes: 1
Reputation: 513
you can use following ways I knew to get the lastest state after updating:
const [state, setState] = useState({name: "Michael"});
const handleChangeName = () => {
setState({name: "Jack"});
}
useEffect(() => {
console.log(state.name); //"Jack"
//do something here
}, [state]);
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;
});
}
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);
}
In react syntheticEvent handler, setState is a batch update process, so every change of state will be waited and return a new state.
"setState() does not always immediately update the component. It may batch or defer the update until later. ",
https://reactjs.org/docs/react-component.html#setstate
Here is a useful link
Does React keep the order for state updates?
Upvotes: 10
Reputation: 125
How about this:
const [Name, setName] = useState("");
...
onClick={()=>{
setName("Michael")
setName(prevName=>{...}) //prevName is Michael?
}}
Upvotes: -3
Reputation: 331
I was running into the same problem, using useEffect in my setup didn't do the trick (I'm updating a parent's state from an array multiple child components and I need to know which component updated the data).
Wrapping setState in a promise allows to trigger an arbitrary action after completion:
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;
The following question put me in the right direction: Does React batch state update functions when using hooks?
Upvotes: 22
Reputation: 557
I had a use case where I wanted to make an api call with some params after the state is set. I didn't want to set those params as my state so I made a custom hook and here is my solution
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];
};
Upvotes: 2
Reputation: 313
setState()
enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state.
setState method is asynchronous, and as a matter of fact, it does not return a promise. So In cases where we want to update or call a function, the function can be called callback in setState function as the second argument. For example, in your case above, you have called a function as a setState callback.
setState(
{ name: "Michael" },
() => console.log(this.state)
);
The above code works fine for class component, but in the case of functional component, we cannot use the setState method, and this we can utilize the use effect hook to achieve the same result.
The obvious method, that comes into mind is that ypu can use with useEffect is as below:
const [state, setState] = useState({ name: "Michael" })
useEffect(() => {
console.log(state) // do something after state has updated
}, [state])
But this would fire on the first render as well, so we can change the code as follows where we can check the first render event and avoid the state render. Therefore the implementation can be done in the following way:
We can use the user hook here to identify the first render.
The useRef Hook allows us to create mutable variables in functional components. It’s useful for accessing DOM nodes/React elements and to store mutable variables without triggering a re-render.
const [state, setState] = useState({ name: "Michael" });
const firstTimeRender = useRef(true);
useEffect(() => {
if (!firstTimeRender.current) {
console.log(state);
}
}, [state])
useEffect(() => {
firstTimeRender.current = false
}, [])
Upvotes: 11
Reputation: 4611
useEffect
is not an intuitive way.I created a wrapper for this. In this custom hook, you can transmit your callback to setState
parameter instead of useState
parameter.
I just created Typescript version. So if you need to use this in Javascript, just remove some type notation from code.
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;
If the passed arrow function references a variable outer function, then it will capture current value not a value after the state is updated. In the above usage example, console.log(state) will print 1 not 2.
Upvotes: 49
Reputation: 437
Your question is very valid.Let me tell you that useEffect run once by default and after every time the dependency array changes.
check the example below::
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;
If you want the setState callback to be executed with the hooks then use flag variable and give IF ELSE OR IF block inside useEffect so that when that conditions are satisfied then only that code block execute. Howsoever times effect runs as dependency array changes but that IF code inside effect will execute only on that specific conditions.
Upvotes: 1
Reputation: 407
UseEffect is the primary solution. But as Darryl mentioned, using useEffect and passing in state as the second parameter has one flaw, the component will run on the initialization process. If you just want the callback function to run using the updated state's value, you could set a local constant and use that in both the setState and the callback.
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);
}
Upvotes: -11