I'm going to assume here that my problem lies within the async nature of how React states work (at least I hope that's a correct statement). I have an app where I have created an UI in which there are 4 buttons with values in them and an "OK" button. The user tries to choose the largest value by clicking the corresponding value button and then clicking "OK" to confirm their choice.
When, why and how does React update my this.setState({ value: this.state.chosenButton }); statement?
Because in the
if (...) {
//...
} else {
this.onAnswer(this.state.value, item.id);
}
part value has still not updated.
I've tried creating a separate function called stateUpdated which houses the setState call, timeouts and other ways of delaying the execution to allow for the state to update, but it seems the issue is not time based but something else completely.
I am also aware that I could just use chosenButton in the last else statement instead of the value but I am more interested in understanding the "Why?" of this problem and not how to "fix" my code as such.
keyInput(event) {
const moduleState = StudentModuleState;
const item: Item = moduleState.displayedItems[0];
const practice: boolean = !StudentModuleState.itemSet.assessment_set;
if (!this || !this._isMounted) { return; }
this.setState({ value: this.state.chosenButton });
if (practice) {
if (this.state.chosenButton === item.correct) {
this.setState({ answerCorrect: true })
setTimeout(() => this.progressHandler(), 2000);
} else {
this.setState({ answerWrong: true, })
setTimeout(() => this.progressHandler(), 2000);
}
} else {
this.onAnswer(this.state.value, item.id);
}
}
I'm going to assume here that my problem lies within the async nature of how React states work...
That's right. The state update is asynchronous, so code immediately following the setState call still sees the old state. To wait until it's updated, use the update callback (the second argument to setState):
keyInput(event) {
const moduleState = StudentModuleState;
const item: Item = moduleState.displayedItems[0];
const practice: boolean = !StudentModuleState.itemSet.assessment_set;
if (!this || !this._isMounted) { return; }
this.setState(
{ value: this.state.chosenButton },
() => {
if (practice) {
if (this.state.chosenButton === item.correct) {
this.setState({ answerCorrect: true })
setTimeout(() => this.progressHandler(), 2000);
} else {
this.setState({ answerWrong: true, })
setTimeout(() => this.progressHandler(), 2000);
}
} else {
this.onAnswer(this.state.value, item.id);
}
}
);
}
Side note about this:
this.setState(
{ value: this.state.chosenButton },
// ------^^^^^^^^^^^^^^^^^^^^^^^
It appears you're updating state in response to a button press (remembering which button was pressed), and then using that updated state in response to a keyboard event. That's okay only because React specifically handles it: It guarantees that the previous state change in response to a click will be rendered (and thus applied) before the next event is dispatched. These used to be called "interactive" events, but now are called "discrete" events, you can find a list here. Note that this is for click and various keyboard events, and not for things like mousemove. Details in this twitter thread, where Dan Abramov (a core committer on the React project) writes:
Even in Concurrent Mode, we indeed guarantee React events like “click” and others that imply intentional user action will flush before the next one is handled. Your “disabled” example is one of the motivations.
Note we don’t guarantee first click is processed synchronously. Only that if you click the next time, we’ll make sure to flush the results of the first one before we decide whether to handle the next event or ignore it.
You can find a list of such events here. (Called “interactive” in code now although that might not be the best naming). https://github.com/facebook/react/blob/master/packages/react-dom/src/events/SimpleEventPlugin.js
We don’t make such guarantees for events like “mousemove” which are continuous rather than discrete. For those we assume it’s safe to batch and sometimes skip intermediate ones as user doesn’t intentionally think about each move as a separate event.
Also note in Concurrent Mode these guarantees only are enforced for React events. If you subscribe manually via addEventListener() there’s some extra stuff you’ll need to do to have them.
However, today (in sync mode) those are always sync. So just something for the future.
Related
I'm trying to write some inter-frame-comunication hook and I'm not sure that the implementation is correct. Unfortunately, the react lifecycle topic seems very complex (example) and I couldn't find a definite answer or recommendation about how to implement it correctly.
Here's my attempt at writing the hook:
const frame = /*...*/;
let messageId = 0;
function usePostMessage(
eventName: string,
handler: (success: boolean) => void
) {
const [pendingMessageId, setPendingMessageId] = useState<number>();
const postMessage = useCallback(() => {
frame.postMessage(eventName);
setPendingMessageId(++messageId);
}, [eventName]);
useEvent(
"message",
useCallback(
(message) => {
if (
message.eventName === eventName &&
message.messageId === pendingMessageId
) {
handler(message.success);
setPendingMessageId(undefined);
}
},
[eventName, handler, pendingMessageId]
)
);
return { postMessage, pendingMessageId };
}
(I'm using useEvent)
Usage:
const { postMessage, pendingMessageId } = usePostMessage(
"eventName",
(success) => {
console.log("eventName", success ? "succeeded" : "failed");
}
);
if (pendingMessageId !== undefined) {
return <div>Pending...</div>;
}
return <button onclick={postMessage}>Click me</button>;
As you can see, I tried to implement a way to post a message and get a response from a frame. I also tried to avoid pitfalls such as getting unrelated responses by keeping a message counter.
It works, but I'm afraid that the "message" event might arrive before the setPendingMessageId state is updated. Is that possible? Are there any guidelines or best practices for implementing this correctly? Thanks.
Update the setPendingMessageId inside the useEffect hook
useEffect(() => {
setPendingMessageId(++messageId);
}, [postMessage])
state update is applied after the postMessage function has been called, avoiding the race condition.
I'm afraid that the "message" event might arrive before the setPendingMessageId state is updated. Is that possible?
No. If a state setter is called inside a React function (such as an onclick prop, as in your code), React will re-render a component after that React handler finishes running its code. JavaScript is single-threaded; once setPendingMessageId(++messageId); is called, the click handler will end, and then a re-render will occur. There's no chance of any other code running before then. The receipt of the message goes through a non-React API (the message listener on the window), so React doesn't try to integrate it into the rerendering flow.
That said, although your code will work, to avoid having to worry about this, some might prefer to reference the stateful values as they are when the message is posted rather than put the logic in a separate hook, which could be less reliable if the state gets out of sync for some other reason. So instead of useEvent, you could consider something along the lines of
const postMessage = useCallback(() => {
frame.postMessage(eventName);
setPendingMessageId(++messageId);
// Save a reference to the current value
// just in case it changes before the response
const thisMessageId = messageId;
const handler = ({ data }) => {
if (data.eventName === eventName && data.messageId === thisMessageId) {
handler(data);
window.removeEventListener('message', handler);
}
};
window.addEventListener('message', handler);
}, [eventName]);
Having a messageId outside of React is a little bit smelly. It'd be nice if you could integrate it into state somehow (perhaps in an ancestor component) and then add it to the dependency array for postMessage.
I have recently done a few API tests for a new job. Just receiving data and passing it through. Although I have completed the tasks and it works functionally, the people I walk through it with are not huge fans of componentDidMount.
They do not suggest an alternative? Anyone know why this could be? Is it due to it being async?
The new modern way to do it is: useEffect
First some code (from the docs):
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
At the end-of-the-day, the componentDidMount purpose is to execute something(the side effect) because the component was mounted(the reason or event).
So you can specify array of dependencies (or causes) for re-running like so:
useEffect(() => {
// ....
}, [someVar]);
so if someVar changed, the function will re-run.
Special use cases are; omitting this argument, will cause it to run once, on-mount event. and specify empty array will cause it to run on each re-render.
For the componentWillUnmount:
Just return a function from the inner function like so:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
I'm have to match a note to an incoming note detected by the microphone. I'm using a fft to detect the pitch and matches that to the next closest note. It triggers the handleNoteEvent about 10x/s. The problem is since setNote is async the if statement == true multiple times, until setState has finished setting the value which subsequently causes the app to rerender multiple times. How can I wait until setState has finished while using react hooks? (currentNote is use by multiple children)
EDIT: setState with hooks doesn't seem to return a promise or take a callback if I understand the docs correctly
EDIT 2: I think I have to clarify my issue: I somehow need to ignore incoming events after the if becomes true, until setState has finished setting currentNote to a new note object.
function App() {
const [currentNote, setNote] = useState(new Note());
//Event handler that gets the event from the fft tuner multiple times a second
const handleNoteEvent = (fftNote) => {
if (currentNote == fftNote)) {
console.log('match');
nextNote();
}
//The problem here is the nextNote() is fired multiple times since setNote is async. How can I ignore all incoming events while setNote is not finished?
const nextNote = () => {setNote(new Note())};
...
}
You will have to handle what happens after the setNote in another useEffect. Something like :
React.useEffect(() => {
// Triggered only once when your component mounts (= componentDidMount in class component)
}, []);
React.useEffect(() => {
// It is only triggered when not changes.
// Do your things after setting notes here.
// Be careful about looping. If you set note here, you'll need to check the value as you did in your exemple to prevent updating note indefinitely
}, [note]);
You are not limited to one useEffect or one hook in general. Split your tasks with multiple useState and useEffect if needed.
Well what I have now works as it should while implementing the useEffect on note change.
It seems to be quite a dirty solution and I would be grateful if anyone could tell me how to do it more clean:
const [currentNote, setNote] = useState(new Note());
var waiting = false;
const handleNoteEvent = (receivedNote) => {
if ((currentNote == receivedNote) && !waiting) {
waiting = true;
setNote(new Note());
}
};
useEffect(() => {
waiting = false;
}, [currentNote]);
I have a screen with some choices on. If you select the choice it sets state of the data. I then have a confirm button. if the user hits confirm I make an async call to get some extra data. I want to wait for this to happen before opening the modal as I need to present that extra data in my modal.
before hooks I would use setState and do something like:
this.setState({data: myData}, () => this.openModal()) as this would reliably set the state then open the modal. all the answers online seem to suggest using useEffect but it seems dodgy to do this:
useEffect(() => {
if (data) {
setModalOpen(true)
}
}, [data, setData])
I don't want my modal potentially randomly opening at different points. plus it seems better to have the code living in the same place I set state. it makes sense to be there. not some random useEffect
any suggestions how this can be achieved?
(one other solution I can think of is making the API call on every choice select, rather than before confirm) however, this could lead to a lot of unnecessary API calls so I'd rather not go down that route.
Using useEffect() is correct, I also encountered this issue when trying to do a callback on setState with hooks.
Like you said: this.setState({data: myData}, () => this.openModal()) was possible before, but now when trying this with hooks the console displays the error:
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().
So useEffect() seems the way to go.
You should use useEffect() as a callback after the state is correctly setted if you would like to do something with the state like validation.
useEffect(() => {
// fetch on load
axios.get("https://randomuser.me/api/").then((response) => {
setPerson(response.data.results[0]);
});
}, []);
useEffect(() => {
// do some validation perhaps
if (person !== null) {
if (person.name.first && person.name.last) {
setModal(true);
} else {
setModal(false);
}
}
}, [person]); // add person in dependency list
As suggested in the comments, you could also do setModal() when the async data has arrived (using .then() or await).
Some example code using random user generator API and axios for fetching.
useEffect(() => {
// fetch on load
axios.get("https://randomuser.me/api/").then((response) => {
setPerson(response.data.results[0]);
setModal(true); // set modal visibility
});
}, []);
I am creating a component with an animation that occurs with a css class toggle. Sandbox of the example here.
The css class is applied conditionaly against the transitioned field, so we should get an animation when the transtioned field goes form false to true.
Problem:
The animation doesn't happen in the case where the state if modified like this :
animateWithoutST = () => {
this.setState({transitioned: false},
() => this.setState({transitioned: true}))
}
But it works if it the second setState is called within a setTimeout callback like this:
animateWithST = () => {
this.setState({ transitioned: false },
() => {
setTimeout(() => this.setState({ transitioned: true }))
}
)
}
Why isn't animateWithoutST working as expected although my component is rendering in the right order ?
This looked definitely bizarre and I had to dig into it until I understood what is happening.
So, yeah without the setTimeout it doesn't work, not even in the componentDidUpdate method and there is an explanation: you are changing the state, and it is updated, and render is called twice BUT because of browser optimization we don't see the animation happening: "browsers are not re-rendering stuff that changed in the same animation frame".
When you use the setTimeout you are forcing the 2nd state update to go into the next animation frame and voila you see the animation. Even with the current timeout value set to 0, it might not work on some browsers, but if you set the value to > 16ms aprox, it prob will work always (you need a value greater than an animation frame).
You can also use requestAnimationFrame, twice to assure your both state updates fall into different animation frames, check it here.
I found all this info in this article, check it because it is extremely well explained. Does now make sense to you?
You should be using componentDidUpdate instead:
componentDidUpdate(prevProps, prevState) {
if(!this.state.transitioned) {
this.setState({
transitioned: true
});
}
}
This makes sure that your second setState gets called AFTER the component has updated.
setState is asyncronous which means calls do not happen back to back. So multiple calls to setstate might not happen as you expect them to. Read this to know more about setstate()