Why react hooks cleanup function is executed when reopening a closed tab? - javascript

I have a problem when I close a tab and then I undo that action. It basically executes all the cleanup functions in the component, aborting the new fetches that are needed to load the component (old cleanup functions are aborting new fetches).
For Chrome and Firefox, it works fine. For what I have tested, it happens only on Safari.
React.useEffect(() => {
const abortController = new AbortController();
fetch(`${URL}${API.mediaDetails}`
+ `?id=${encodeURIComponent(props.match.params.id)}`
+ `&type=${encodeURIComponent(props.match.params.type)}`
+ `&lang=${encodeURIComponent(props.match.params.locale?.slice(0, 2))}`, {
signal: abortController.signal,
timeout: DOWNLOAD_HUB_REQ_TIMEOUT
}).then((res) => res.json()).then((res) => {
res.type = props.match.params.type;
return setDetails(res);
}).catch((err) => {
setDetails(null);
setError(err);
});
return () => abortController.abort();
}, [props.match.params]);

The cleanup function (to abort the request) will be run whenever the component is removed from the DOM. When you close the tab, that component (and in fact, all of the components on the page) will be removed, which means all their cleanup functions will be run.
This is normally what you want. If you go to /foo/1, and you start fetching data for foo #1 ... but then you change pages to /foo/2 ... you want to abort the fetch for foo #1! You don't need it anymore, so why have it slow down the request for foo #2?
However, if you for some reason don't want this behavior, you could always set some sort of flag (eg. dontCleanup) before the user leaves, and then check for that flag in your cleanup function.
NOTE: Individual browsers (eg. Safari) may have bugs that violate this behavior. Upgrading to the latest browser version should resolve it.

Related

alternatives to componentDidMount....Axios?

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);
};
});

service worker doesn't skip waiting state

I'm still doing experiments in order to master service workers, and I'm facing a problem, probably because of my lack of expertise in JavaScript and service workers.
The problem happens when I want the new service worker to skipWaiting() using postMessage(). If I show a popup with a button and I bind a call to postMessage() there, everything works. If I call postMessage() directly, it doesn't work. It's a race condition because SOMETIMES it works, but I can't identify the race condition.
BTW, the postMessage() call WORKS, the service worker is logging what it should when getting the message:
// Listen to messages from clients.
self.addEventListener('message', event => {
switch(event.data) {
case 'skipWaiting': self.skipWaiting(); console.log('I skipped waiting... EXTRA');
break;
}
});
Here is the code. The important bit is on the if (registration.waiting) conditional. The uncommented code works, the commented one doesn't:
// Register service worker.
if ('serviceWorker' in navigator) {
// Helpers to show and hide the update toast.
let hideUpdateToast = () => {
document.getElementById('update_available').style.visibility = 'hidden';
};
let showUpdateToast = (serviceworker) => {
document.getElementById('update_available').style.visibility = 'visible';
document.getElementById('force_install').onclick = () => {
serviceworker.postMessage('skipWaiting');
hideUpdateToast();
};
document.getElementById('close').onclick = () => hideUpdateToast();
};
window.addEventListener('load', () => {
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});
navigator.serviceWorker.register('/sw.js').then(registration => {
// A new service worker has been fetched, watch for state changes.
//
// This event is fired EVERY TIME a service worker is fetched and
// succesfully parsed and goes into 'installing' state. This
// happens, too, the very first time the page is visited, the very
// first time a service worker is fetched for this page, when the
// page doesn't have a controller, but in that case there's no new
// version available and the notification must not appear.
//
// So, if the page doesn't have a controller, no notification shown.
registration.addEventListener('updatefound', () => {
// return; // FIXME
registration.installing.onstatechange = function () { // No arrow function because 'this' is needed.
if (this.state == 'installed') {
if (!navigator.serviceWorker.controller) {
console.log('First install for this service worker.');
} else {
console.log('New service worker is ready to activate.');
showUpdateToast(this);
}
}
};
});
// If a service worker is in 'waiting' state, then maybe the user
// dismissed the notification when the service worker was in the
// 'installing' state or maybe the 'updatefound' event was fired
// before it could be listened, or something like that. Anyway, in
// that case the notification has to be shown again.
//
if (registration.waiting) {
console.log('New service worker is waiting.');
// showUpdateToast(registration.waiting);
// The above works, but this DOESN'T WORK.
registration.waiting.postMessage('skipWaiting');
}
}).catch(error => {
console.log('Service worker registration failed!');
console.log(error);
});
});
}
Why does the indirect call using a button onclick event works, but calling postMessage() doesn't?
I'm absolutely at a loss and I bet the answer is simple and I'm just too blind to see it.
Thanks a lot in advance.
Looks like a bug in Chromium or WebKit, because this code works all the time in Firefox but fails in Chrome and Edge most of the time.
I've reported that bug to Chromium, let's see if it is a bug or my code is weird. I've managed to build the smallest code possible that still reproduces the issue, it's quite small and it can be found on the bug report.
The report can be found here.
Sorry for the noise, I'll keep investigating the issue but no matter how I tried, the code still fails from time to time, I can't spot the race condition on my code.

How to skip running useEffect on first run is Strict mode?

I have a custom hook that I use when I need to skip running of the useEffect function on a first call. This works like a charm in non Strict mode.
hooks/useEffectSkipFirst.js
import { useCallback, useEffect, useRef } from 'react';
export default (fn, deps) => {
const isFirstRun = useRef(true);
const execFunc = useCallback(fn, deps);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
execFunc();
}, [execFunc])
};
Usage:
import useEffectSkipFirst from 'hooks/useEffectSkipFirst';
useEffectSkipFirst(() => {
// fetch new user by ID
}, [selectedUser.id])
Obviously, this hook works completely differently in Strict mode where it will execute a function on the first call because the useEffect hook will be called twice. The reason why it will be called twice is the fact that its dependency is a variable returned from useState (or react-redux's useSelector) which gets called twice in Strict mode. Therefore it makes an API call on the first run while it shouldn't until the user changes.
The only way I can fix this is by listening to NODE_ENV variable, and in case of development, I would block execution of the function until 3rd call while blocking until 2nd call in case of other envs. Unfortunately, that sounds like a poor solution to me.
Is there a better way to write a custom hook that works in both modes? Bear in mind that it is important to write a hook that doesn't throw any eslint-plugin-react-hook warnings.
EDIT: Epic facepalm on my end. The reason why everything works for me in non Strict mode and doesn't in Strict is that all my routes are rendered through Private.js component which checks whether I already have a user in store (logged in) or not. In case I don't have a user it will make an API call. Because I'm doing the check within render function (wrong to have side effects in that check) and render is called twice in Strict mode I'm actually making 2 API calls (bug). That also means my Redux store gets updated twice and thus triggers my useEffect twice which has that variable as a dependency. I've been looking for the bug in the wrong place the whole time. I apologize for wasting your time.
Don't know if you still need my answer but I could come across with this workaround:
setTimeout(() => (isFirstRender.current = false), 1000);
By setting the ref to false only after 1 second of timeout, React can run its useEffect as often as it can, the if check will fail on the first render. With that I can easily start my countdown timer only onClick so if my dependency changes.

React Performance Issues in Firefox?

I'm experiencing some performance issues with a react application that I developed. These issues specifically (or most notably) occur with Firefox (both FF developer 77.0b7 and FF 76.0.1).
When using this application in Firefox, CPU usage gets extremely high, and my fans start spinning up to very high speeds. I get about 15-19fps in firefox according to the performance tools in FF. I get roughly 60fps in Chrome and Safari.
These issues occur when I begin typing into the input field, and get worse as the input gets longer (which makes sense)
The application is available here:
https://text-to-aura-generator.netlify.app/
Source code available here: https://github.com/paalwilliams/Text-to-Aura/tree/master/src
I'm almost certain that this is something I'm doing incorrectly, or that I've written the code inefficiently, but that isn't necessarily supported by the stark performance difference between browsers. Is chrome just that much better and handling react/constant rerenders?
I know that this is a broad question, but I honestly don't understand what is happening here, or necessarily how to troubleshoot it beyond the developer tools. Any input or thoughts would be greatly appreciated.
The problem is your application is rendering too fast. In your particular case, there a few ways to improve that.
Every time you update the state, React needs to re-render your application, so updating the state within a loop is usually a bad idea.
Also, you are using useState 3 times, but only colors should be there, as App actually needs to re-render to reflect the changes there. The other two pieces of state (text and hex) are only being used to pass data from the handleChange to the callback inside useEffect.
You can restructure your code to:
Avoid updating the state within a loop.
Use a simple variable instead of state.
Use useCallback to define a function with that logic that is not re-created on each render, as that forces TextInput to re-render as well.
Throttle this callback using something like this:
import { useCallback, useEffect, useRef } from 'react';
export function useThrottledCallback<A extends any[]>(
callback: (...args: A) => void,
delay: number,
deps?: readonly any[],
): (...args: A) => void {
const timeoutRef = useRef<number>();
const callbackRef = useRef(callback);
const lastCalledRef = useRef(0);
// Remember the latest callback:
//
// Without this, if you change the callback, when setTimeout kicks in, it
// will still call your old callback.
//
// If you add `callback` to useCallback's deps, it will also update, but it
// might be called twice if the timeout had already been set.
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Clear timeout if the components is unmounted or the delay changes:
useEffect(() => window.clearTimeout(timeoutRef.current), [delay]);
return useCallback((...args: A) => {
// Clear previous timer:
window.clearTimeout(timeoutRef.current);
function invoke() {
callbackRef.current(...args);
lastCalledRef.current = Date.now();
}
// Calculate elapsed time:
const elapsed = Date.now() - lastCalledRef.current;
if (elapsed >= delay) {
// If already waited enough, call callback:
invoke();
} else {
// Otherwise, we need to wait a bit more:
timeoutRef.current = window.setTimeout(invoke, delay - elapsed);
}
}, deps);
}
If the reason to use useEffect is that you were not seeing the right values when updating colors, try using the version of setState that takes a callback rather then the new value, so instead of:
setColors([...colors, newColor]);
You would have:
setColors(prevColors => ([...prevColors , newColor]));
The most common performance issues with react come from setting the state too many times since you're constantly re rendering the page and the elements within it.

socket.io listener firing too many times in functional React

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
})

Categories