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.
Related
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
});
}, []);
setState updates state asynchronously. It's my understanding that, when using a class component, you can do something like this to ensure certain code is executed after one setState updates state:
setState({color: red}, callbackThatExecutesAfterStateIsChanged);
I'm using a functional component & hooks. I'm aware, here, useEffect()'s callback will execute everytime after color state changes and on initial execution.
useEffect(callback, [color]);
How can I replicate similar behaviour as the class component example - that is, to execute a chunk of code once after one setState() successfully changes state and not on initial execution?
If you ask me, there is no safe way to do this with hooks.
The problem is that you both have to read and set an initialized state in order to ignore the first update:
const takeFirstUpdate = (callback, deps) => {
const [initialized, setInitialized] = useState(false);
const [wasTriggered, setWasTriggered] = useState(false);
useEffect(() => {
if (!initialized) {
setInitialized(true);
return;
}
if (wasTriggered) {
return;
}
callback();
setWasTriggered(true);
}, [initialized, wasTriggered]);
};
While the hook looks like it works, it will trigger itself again by calling setInitialized(true) in the beginning, thus also triggering the callback.
You could remove the initialized value from the deps array and the hook would work for now - however this would cause an exhaustive-deps linting error. The hook might break in the future as it is not an "official" usage of the hooks api, e.g. with updates on the concurrent rendering feature that the React team is working on.
The solution below feels hacky. If there's no better alternative, I'm tempted to refactor my component into a class component to make use of the easy way class components allow you to execute code once state has been updated.
Anyway, my current solution is:
The useRef(arg) hook returns an object who's .current property is set to the value of arg. This object persists throughout the React lifecycle. (Docs). With this, we can record how many times the useEffect's callback has executed and use this info to stop code inside the callback from executing on initial execution and for a second time. For example:
initialExecution = useRef(true);
[color, setColor] = useState("red");
useEffect(() => {
setColor("blue");
});
useEffect(() => {
if (initialExecution.current) {
initialExecution.current = false;
return;
}
//code that executes when color is updated.
}, [color]);
I have this state:
const [fingerprintingIssues, setFingerprintingIssues] = React.useState([])
I have a function that loops through a bunch of data to validate. If there are errors it adds them to fingerprintingIssues with setFingerprintingIssues. That works fine.
But in this method (abbreviated) I’m checking for fingerprintingIssues inside of the method’s complete and it always returns []. I’ve read this is an issue with closures and stale state, but I can’t seem to see how that applies here.
const validateDocument = data => {
const start = moment()
try {
Papa.LocalChunkSize = 10485
return Papa.parse(data, {
complete: () => {
// Log completion only if it’s been 20 seconds or more
const end = moment()
const duration = moment.duration(end.diff(start))
const seconds = Math.ceil(duration.asSeconds())
if (seconds < MIN_PROCESS_TIME) {
setTimeout(() => {
setUploadStep('tagging')
console.log('fingerprintingIssues', fingerprintingIssues)
}, (MIN_PROCESS_TIME - seconds) * 1000)
} else {
setUploadStep('tagging')
}
},
error: () => {
setDocumentError(true)
setDocumentReady(false)
},
})
…
Any suggestions?
I think indeed it is an issue with closure and stale state. When using hooks, you need to be cautious about how you use callbacks.
Your component function is called many times (one for each render). Each call will receive a particular state. You can sort of think of it like that particular state is associated with that particular render. If you create a callback within your render, that callback will exist inside the closure of the render it was created in. The issue is that if your component is re-rendered, the callback still exists in the closure of the old render, and will not see the new state.
One way around this is to use React.useRef, which you can read more about here. Refs hold a value and can be modified and accessed at any time regardless of closure. Note that modifying the ref does not cause your component to be re-rendered.
const fingerPrintingIssues = React.useRef([])
const validateDocument = data => {
// Perform validation
// Now access (refs are accessed by .current)
console.log(fingerPrintingIssues.current)
}
// Modify:
fingerPrintingIssues.current.push(...)
In case you need to re-render your component when modifying the value, you can use combination of useRef() and useState().