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);
};
});
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 know that a watch runs before the beforeUpdate lifecycle event occurs. However, I have things in the watch which must complete before the virtual-DOM is re-rendered/updated.
My watchis like this:
setup () {
watch(() => $route.params.ProductID, async () => {
// Update Vuex here with ProductID data and then do a whole bunch of stuff here that must complete before updating the DOM
},
{
deep: true,
immediate: true
})
onBeforeUpdate(() => {
// Use stuff that was created in watch above
})
}
This code is not only run by the client, but also the server for SSR.
I have tried to test if watch will complete all tasks first before onBeforeUpdate() runs like this:
setup() {
async function testWatch(){
setTimeout(function(){ return console.log("I am watcher timeout"); }, 3000);}
setup () {
watch(() => $route.params.ProductID, async () => {
await testWatch();
},
{
deep: true,
immediate: true
})
onBeforeUpdate(() => {
console.log("I am onBeforeUpdate hook");
})
}
}
If I run the above I get the output of:
I am onBeforeUpdate hook
then 3 seconds later..
I am watcher timeout
How can I ensure that the code within the watch will execute before anything else in the component? I need this to happen so that the correct data is available to the component before it renders.
How can I ensure that the code within the watch will execute before anything else in the component?
You can't - it is not possible.
Your best bet is to move the async code to route guards as described in the docs - Fetching Data Before Navigation
Problem with the approach is (or can be - sometimes) that the fetching code (getPost function in the example) cannot be a method on the component and (logically) has no access to this
I solve this sort of thing by creating a data property 'loading' that gets set to false when loading is done.
Then use a v-if to only render stuff when 'loading' is false.
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]);
Why does the load() function with [] as the second argument not run on mount unless I pass in [load] to the second argument? I do not want to pass in the second argument as I only want the load() function to run once on mount and never afterward. How do I achieve this?
const HoursTimer = (props) => {
const [isActive, setIsActive] = useState(null);
const [activeId, setActiveId] = useState(null);
useEffect(() => {
load();
}, [])
function load() {
props.hours.forEach((hour, i) => {
if (i === (props.hours.length-1)) {
setIsActive(hour.clockedIn)
setActiveId(hour._id)
}
})
}
return (
<div>
<p>{isActive}</p>
<p>{activeId}</p>
</div>
);
};
function mapStateToProps( { hours } ) {
return { hours };
}
export default connect(mapStateToProps, { fetchHours })(HoursTimer);
I've never used React before, but a quick review of the useEffect source, and I think I have and answer for you.
useEffect takes two arguments, as you know. But did you know the array, aka "deps" tells React, when you want to rerun the effect? React does this by "rechecking" those deps.
So a short hand for run once, but never again is an empty array.
I confirmed this from React's documentation
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values. While passing [] as the second
argument is closer to the familiar componentDidMount and
componentWillUnmount mental model, there are usually better solutions
to avoid re-running effects too often. Also, don’t forget that React
defers running useEffect until after the browser has painted, so doing
extra work is less of a problem.
Another insightful comment from the documentation
The array of dependencies is not passed as arguments to the effect
function. Conceptually, though, that’s what they represent: every
value referenced inside the effect function should also appear in the
dependencies array. In the future, a sufficiently advanced compiler
could create this array automatically.
So depending on you desired behavior, if you want to run the effect, everytime the prop.hours updated you might need to do something like
useEffect(() => {
load();
}, [props.hours])
function load() {
props.hours.forEach((hour, i) => {
if (i === (props.hours.length-1)) {
setIsActive(hour.clockedIn)
setActiveId(hour._id)
}
})
}
Looking into things a bit more, I found this article, which may be helpful.
As for you question:
Why does the load() function with [] as the second argument not run on
mount unless I pass in [load]
I suspect, it's because [load] is a function, which react, will rerun, but
it's doing so to check if it needs to rerun the effect... with the side effect, of well, having already, rerun load, which just so happens to be your entire useEffect. -- that was at least 3 levels of inception.
Hope that helps.