I have a timer in my component, which is running with setInterval that starts in mounted() component.
Suppose this component is at http://localhost:3000/some-route.
Now how do I do clearInterval() whenever I go to another route like http://localhost:3000/ as I want to stop the timer.
I've used unmounted() but when you go to different route, the component doesn't unmounts but if I go to same route (/some-route), setInterval runs again as the component is mounted again.
So, how do I clear the interval every time I go to different route?
I had to do it once, it's a bit tricky because of the scope and the way it interacts with this (arrow functions basically). At the end, I also needed it to be available globally so I used it in pair with Vuex but you can totally use it in a component with some upper scope for the variable.
Here is a sample of code I've used
actionSetPolling({ state, dispatch }) {
try {
const myPolling = setInterval(async function () {
if (someVariable !== 'COMPLETED') {
// do stuff
} else if (conditionToStop) {
// this is facultative, but can be done here so far too
window.clearInterval(myPolling)
}
}, 2000)
dispatch('setPollingId', myPolling)
} catch (error) {
console.warn('error during polling', error.response)
window.clearInterval(state.pollingId)
}
}
setPollingId is an action that set's a pollingId mutation, that way I can track it all along globally.
You could use the same while unmounting your component with this
beforeDestroy() {
window.clearInterval(state.pollingId)
},
Not sure if it's the best way of doing things but setInterval is by it's nature, clunky and hacky out of the box IMO, especially in an SPA.
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 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]);
index.js:1 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().
This is the error I get when trying to do this
setUnansweredQuestions({ newUnansweredQuestions }, () =>
nextQuestion()
);
I tried run a function after updating the state for unanswered questions but won't work cause it doesn't update right away.
I searched a bit and it is said to use useEffect but I already have one defined and won't let me create another. I just want to call the function nextQuestion after updating UnansweredQuestions
useEffect(() => {
setUnansweredQuestions(questions);
selectRandomQuestion();
}, [currentQuestion]);
There's nothing wrong with having multiple useEffects.
Since you do setUnansweredQuestions and want to run something after that state variable changes, just do:
useEffect(nextQuestion, unansweredQuestions);
You can try like this, I know this is not a good solution, but it works
setState(UnansweredQuestions);
setTimeOut(() => {
nextQuestion()
}, 16)
I am building an online boardgame usin create-react-app, react hooks, and am using sockets.io to transmit data (player location, active player, etc.) between connected users. The flow of the logic is that a user makes a choice, that choice gets added to an array in state, and then the updated state is pushed via sockets to all connected users. The problem is that the useEffect listener that is in charge of receiving the socket data from the back end and updating the user data on each connected user is firing too many times instead of just once.
Code:
Send call to the back end:
try {
console.log(currentCard, typeof(currentCard.title), tech)
setUser1Data({
...user1Data,
userTech: [...user1Data.userTech, currentCard.title]
});
} finally {
console.log(user1Data)
socket.emit("p1state", user1Data);
pass();
}
The back end receiver/emitter:
socket.on("p1state", function(state) {
console.log(state)
io.emit("p1state", state)
})
The client listener:
useEffect(() => {
socket.on("p1state", state => {
console.log("1")
setUser1Data({...user1Data, state});
});
}, [user1Data]);
Some "interesting" things I noticed: this useEffect is being fired too many times. The first time it fires it sets everything the way it should, but then each subsequent time it overwrites the previous setting, reverting to the original user1Data state object.
Also, on the back end, I have a console.log firing when a client connects. Even though I am testing only locally with one browser tab at the moment, it is still logging several user connected events.
The useEffect is currently using the state in the dependency array and is setting the same state in the updater function. As you can see, this leads to an infinite loop.
useEffect(() => {
socket.on("p1state", state => {
console.log("1")
setUser1Data(userData => ({...userData, state}));
});
}, []);
Instead you can use the function version of state setter so that it gives you the accurate prevState instead of relying on state representation in closure.
I had a similar problem. I solved it making the useEffect close the socket every time it gets unmounted (and open/reopen after every mount/update. This was my code:
useEffect(()=>{
const socket = io("http://localhost:3000")
socket.on(userId, (arg) => {
//stuff
});
return () => socket.emit('end'); //close socket on unmount
})