How can I do a callback after useState with a pure function? - javascript

useEffect(() => {
if (selectedSampleJobIndex !== undefined) {
setAppState((s) => ({
...s,
resumeInput: sampleJobValues[selectedSampleJobIndex],
}));
}
}, [selectedSampleJobIndex]);
I want to do something once that appState is done. But it won't accept a second callback function.
How can I best handle this?
I can't easily use useEffect, because what's being changed is in a top level context and might also change due to user input

You can accomplish this by calling your callback function in the updater function, like so:
useEffect(() => {
if (selectedSampleJobIndex !== undefined) {
setAppState((s) => {
cb(); // Call your callback function here. You can pass it previous or next state
return {...s, resumeInput: sampleJobValues[selectedSampleJobIndex]};
});
}
}, [selectedSampleJobIndex]);
This is the closest hooks equivalent to the setState callback parameter that can only be used in class components.
Note that the updater function should be a pure function, therefore be mindful of the effects of your callback. In principle, there should not be a need to invoke a callback after setting state with hooks. You can trigger an effect when the state changes by using the useEffect hook and including the state in the dependency array.

Related

Execute useEffect() only on specific setState()

I am using a hook component and several state variables. I have read about using useEffect() with params to get a kind of callback after updating a state. Example:
export const hookComponent = () => {
const [var, setVar] = useState(null);
useEffect(() => {
//do things
}, [var])
}
In this example, useEffect() would be executed on every setVar() call. In my case, I do not want to execute useEffect() everytime, but only on specific occasions.
I would like to give the setVar() some kind of information which I can use in useEffect() like setVar(newValue, true).
Note: I do not want to store this information in var.
Is there a way to do this?
Like Nizar said, simple conditional check on 'var' in useEffect
If expensive calc you can
const expensiveValue = useMemo(() => {
// other logic here if needed
// could even be simple return var=='x'?true:false, although this would be easier to do in the useEffect hook?
return computeExpensiveValue(var);
},[var]);
useEffect(() => {
//do things
//expensiveValue only changes when you want it to from the memo
}, [expensiveValue])
Thank you sambomartin and Nizar for your input.
For everyone looking for an answer:
After some further research I found 3 possible solutions:
Use a class component. If you really are dependent on that state update to be completed switch to a class component, which allows you to give the setState() a callback as a second param.
Use the useRef hook to determine where your state update is comming from. You can use this information in the useEffect() method.
Get independent from the state. I used this solution and externalized my callback function with the drawback of giving it every parameter on every call, although they are present in the component the states are saved.
As far as I know, the useEffect only triggers if the dependency value changes, not simply by executing setValue.
I offer you three solutions, the first one, close to what you want but without using useEffect hook, the second one is an extension of the first one, that may be required if you need control over the previous state, and the third, more general, like comments say, though it won't be triggered if the state is the same, even if you execute setValue.
First solution: Wrap your set value with another function that definitely controls what may happen after or before the new state:
export default function MyComponent() {
const [state, setState] = useState(null);
const handleChangeSetState = (nextState, flag) => {
if (flag) {
specialUseCaseCb();
}
setState(nextState);
};
return <div>{/* ... */}</div>;
}
Second solution: Wrap your set value with another function, like in the solution 1, and ask for the previous or next state within setState inner callback:
export default function MyComponent2() {
const [state, setState] = useState(0);
const handleChangeSetState = (increment, flag) =>
setState((prevState) => {
const nextState = prevState + increment;
// you may need prevState or nextState for checking your use case
if (flag) {
specialUseCaseCb();
}
return nextState;
});
return <div>{/* ... */}</div>;
}
Third solution: use useEffect hook to follow changes, remember though that setState won't re-trigger useEffect hook if the state is the same:
export default function MyComponent3() {
const [state, setState] = useState("");
// notice that this will only be triggered if state changes
useEffect(() => {
if (state !== "my-special-use-case") return;
specialUseCaseCb();
}, [state]);
return <div>{/* ... */}</div>;
}

What is correct way to call function inside setState(useState) hooks

I want to update state at real time and call function inside setState but i am getting error.
onClick={() => setDetails(prevState => ({...prevState,id: product.orderId,status: 'processing'}) => updateStatus() )}
Can someone tell me correct syntax and expression
It seems you are using hooks. You can't call a function in setState like in class components. You need to watch it through useEffect.
React.useEffect(() => {
updateStatus()
}, [state.id, state.status])
the function created using useState hook doest have a second argument. it will not support call back function after updating the state. it was only there in the setState function which is used in class components.
You can use useEffect instead
useEffect(()=> {
updateStatus()
}, [details])
return (
...
onClick={() => setDetails(prevState => ({...prevState,id: product.orderId,status: 'processing'}))}
...
)
in this way, on every setDetails update it will call useEffect and then updateStatus

Do React state updates occur in order when used like an FSM and useEffect?

I am trying to build a simple FSM (finite state machine) in ReactJS functional components. I am looking at this which specifies that they it can do it asynchronously and second they may merge the state changes.
In class components, you can chain state changes using the two-argument setState() method. But that capability is not available in the useState() hook.
Basically I am wondering because of the behaviour specified is it possible to create an FSM? I am trying to trim the example as small as I can
I am making a few restrictions on my usage of the setter, namely it will only be executed in:
a state handler function
an event handler (and even this is limitted)
a state can have its own substate which is another state store (basically additional parameters that may only be read or written by the two above functions)
Anyway I set my initial state and I have a single useEffect hook for the state as follows
const [fsmState, setFsmState] = useState(States.Initial)
useEffect(() => {
stateHandlers[fsmState]();
}, [fsmState]);
States is an enum
enum States {
Initial,
Unauthenticated,
Authenticated,
Processing
}
I define stateHandlers as follows:
const stateHandlers: {
[key in States]: () => void | Promise<void>;
} = {
[States.Initial]: async () => {
// this may load authentication state from a store,
// for now just set it to Unauthenticated
setFsmState(State.Unauthenticated);
},
[States.SigningIn]: async () => {
const authToken = await loginToServer(signInState));
if (authToken.isValid()) {
setFsmState(State.Authenticated);
} else {
setFsmState(State.Unauthenticated);
}
},
[States.Authenticated]: () => { },
[States.Unauthenticated]: () => { },
}
For the event handler e.g. sign in I do this (which is basically the reason I am asking the question.
const signinAsync = async (username: string, password: string) => {
if (fsmState !== States.Unauthenticated) throw Error("invalid state");
// would these two get set in order?
setSignInState({username,password})
setFsmState(State.SigningIn);
}
As specified earlier, the two-argument version is not available so I cannot do
setSignInState({username,password},
() => { setFsmState(State.SigningIn) } )
I just wanted a simple TSX implementation to avoid adding additional libraries. However, I am still in the midst of coding this as I learn TypeScript/React.
Yes it is possible. In fact,there is a well known FMS library called xstate https://github.com/davidkpiano/xstate.
Updating state will always be asynchronous because that is how it works and rightly so. In your state machine handler, you just need to make sure that the transition is correct. I can already see that your implementation is already heading to the right direction.
const signinAsync = async (username: string, password: string) => {
if (fsmState !== States.Unauthenticated) throw Error("invalid state");
// would these two get set in order?
setSignInState({username,password})
setFsmState(State.SigningIn);
}
There is no guarantee which one will happen first. Your implementation should handle all cases, whether setSignInState or setFsmState happens first. Both actions should be independent. If you need both to be in a series then you need to chain them in a callback or something. For example, with setState you can pass a second arguments as a callback that will be called after state is updated.
this.setState({ title: event.target.value }, function() {
this.validateTitle(); // thid will happen after state update
});

Why does state not update as expected?

I am currently building a TicTacToe game and would like to store my current player in state as currentPlayer. After one player moves, I update currentPlayer to the opposite player. However, when I try to log the new state to the console, it's not producing the value of the updated state.
Here is my code:
state = {
currentPlayer: 'X',
}
// This function is triggered by an onClick attribute.
// It first finds the html element and renders the currentPlayer value from state.
// It then switchs the value of currentPlayer from X to O and calls setState to update the state.
// Why does the console.log not show the updated state value?
userDidMove = () => {
document.getElementById('cell').innerHTML = this.state.currentPlayer
let nextPlayer = this.state.currentPlayer === 'X' ? 'O' : 'X'
this.setState({
currentPlayer: nextPlayer,
})
console.log ('userDidMove changed state with ',this.state.currentPlayer)
}
Any help figuring out how to get this function to return the updated state value would be great!
State changes are asynchronous. When your new state is dependent on the previous state, use the state updater function instead.
When the state changes are committed you can use the callback which will have the updated state.
this.setState((previousState) => {
const nextPlayer = previousState.currentPlayer === 'X' ? 'O' : 'X';
return {
currentPlayer: nextPlayer
}
}, () => {
// your updated state related code here
console.log('userDidMove changed state with ', this.state.currentPlayer)
});
this.setState(updatedFunc, callback);
setState is asynchronous, so the state isn’t updated immediately. You can pass a callback as the second argument to setState that will only be called when state has been updated:
this.setState(
{ currentPlayer: nextPlayer },
() => console.log(`userDidMove changed state with ${this.state.currentPlayer}`)
);
setState (React Docs):
setState(updater[, callback]) OR setState(stateChange[, callback])
Think of setState() as a request rather than an immediate command
to update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
NOTE: I suggest observing state using the React Dev Tools, instead of logging it.
UPDATE: This answer initially stated, incorrectly, that setState returned a promise and suggested that you could chain .then() that would be called once state was updated. I've since corrected the answer, with inspiration from #Sushanth's answer.
State changes are asynchronous. so use a function instead and the second parameter of setState function you may call the callback function to console or something else to do.
this.setState(() => ({currentPlayer: nextPlayer}), () => {
console.log('state', this.state.currentPlayer);
})

How to handle asynchronous calls to setState in React?

I have a method that toggles a boolean value in state by copying the value then updating state:
toggleSelected = () => {
let selected = this.state.lists.selected;
selected = !selected;
this.setState({
// update state
});
};
I have another method, fired by an onClick handler, that calls toggleSelected twice:
switchList = (listName) => {
const currList = this.getCurrentList();
if(listName === currList.name) return;
this.toggleSelected(listName);
this.toggleSelected(currList);
};
However it appears that the state doesn't finish getting set from the first call by the time the second call runs; if I set a timeout on the second call, it works fine.
What's the correct way to do this?
An alternative to what #SLaks suggested, useful sometimes, is using the setState(new_state, callback) method. The callback will be run once the state is updated and "visible".
In general, setState will not change the state immediately so that it is visible in this.state. From the docs
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
As noted in the React State/Lifecycle docs, the correct way to update state based on previous state is by passing a function into the setState() call, eg:
toggleSelected() {
this.setState((prevState, props) => {
return {
lists: {
selected : ! prevState.lists.selected
}
};
});
}
This is why you should always pass a lambda to setState(), so that it will give you the actual current state:
this.setState(state => ({ ..., selected: !state.lists.selected }))

Categories