React 18 strict mode causing component to render twice - javascript

The changes to strict-mode in React version 18 causes my code to render twice, which causes an error in axios abort controller, but I don't know how to clear the error from the browser console after the app renders twice.
Please note: I am working on a sign-up / log-in app and even after I successfully logged in, React takes me back to the log-in page, because of the axios error
useEffect(() => {
let isMounted = true;
// used by axios to cancel request
const controller = new AbortController();
const getGoals = async () => {
try {
const response = await goalPrivate.get("/goals", {
// option to cancel request
signal: controller.signal
})
console.log(response?.data);
// set goals state when component mounts
isMounted && setGoals(response?.data);
} catch (error) {
console.log(error.message);
// when refreshToken expires
navigate("/login", { state: { from: location }, replace: true });
}
}
getGoals();
// cleanup function
return () => {
// don't set state if component unmounts
isMounted = false;
// cancel request if component unmounts
controller.abort();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

React StrictMode calls all Effects twice to make sure their cleanup/unmount handlers work as intended. You may need to change your effects accordingly, even if they have an empty dependency list and would normally not unmount before the site is closed.
Note, this only happens in Strict + development mode. In a production build, effects will only be called once and when their dependencies change.
Fore more context, see https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state

Had the same problem and fixed it this way.
When the abortController is aborted you jump to the catch so you just check if the signal is aborted or not to execute the rest of your code.
useEffect(() => {
const abortController = new AbortController();
fetch("https://pokeapi.co/api/v2/pokemon", {
signal: abortController.signal,
})
.then((res) => res.json())
.then(console.log)
.catch((err) => {
if (abortController.signal.aborted) return;
console.log(err);
// Your navigate
});
return () => {
abortController.abort();
};
}, []);

If you have the StrictMode enabled, it will fire two times the useEffect on development mode to make sure that you are aware of the possible side-effects that could appear.

You should classify the error response depends on error code or http status code.
Eg:
...
try {
// Create axios request
} catch (e: AxiosError) {
if (error.code === 'ERR_CANCELED') {
// When abort controller executed
} else (error.response.status == 401) {
// When you get http code 401 (Un-authenticated)
// Eg:
navigate("/login", { state: { from: location }, replace: true });
} else {
// Etc...
}
}
...

React 18 now has Strict.Mode mount, unmount, and remount components which causes the abortController to issue an error on the first unmount. Remember, this only happens in development mode when Strict.Mode is applied in your index.js. We can check for that behaviour while in development-mode.
try {
// fetch API data
} catch (error) {
if (process.env.NODE_ENV === "development" && error) {
// ignore the error
console.log(error.message);
} else {
// when refreshToken expires, go back to login
navigate("/login", { state: { from: location }, replace: true });
}
}

Related

how to properly cleanup useEffect that contain async graphql operation

I am using graphql/apollo and react.
I have the following code
const [state, setState] = useState(undefined);
useEffect(() => {
(async () => {
try {
const workspace = await getFirstWorkspace();
// Do Something
setState(withSomething)
} catch (error) {
// Do Something Else
setState(withErrorSomething)
}
})();
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
now, this worked fine until I updated some packages, I currently get thrown this error.
Uncaught (in promise) DOMException: signal is aborted without reason
From what I understand my useEffect throw this when the component is unmounted an the query didn't finish to run.
Now, this cause my catch to always trigger at least once, cause it looks like when the effect is run again cause one of the dep changed, it fail.
I """ fixed """ it by doing
const [state, setState] = useState(undefined);
useEffect(() => {
(async () => {
try {
const workspace = await getFirstWorkspace();
// Do Something
setState(withSomething)
} catch (error) {
// Do Something Else
if ((error as any)?.name === 'AbortError') {
return;
}
setState(withErrorSomething)
}
})();
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
And not assign any state in case the error is an abort. But I couldn't find any proper solution or I don't understand why this is problematic before and not now, I did update some package but none mention a change of behavior on this end.
My question is, what should I do to do thing correctly ?
I don't think the error you've quoted is coming from React. React used to complain if you did a state update in a component that was no longer mounted, but the error message it used was "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application." But recent versions of React don't do that because the React team decided it was too fussy.
Still, answering the question as asked:
If getFirstWorkspace offers a way to tell it to cancel what it's doing, you'd do that. For instance, if it supported AbortSignal, you might do this:
useEffect(() => {
// *** Create a controller and get its signal
const controller = new AbortController();
const { signal } = controller;
(async () => {
try {
// *** Pass the signal to `getFirstWorkspace`
const workspace = await getFirstWorkspace(signal);
// *** Only do something if the signal isn't aborted
if (!signal.aborted) {
// Do Something
setState(withSomething);
}
} catch (error) {
// *** Only do something if the signal isn't aborted
if (!signal.aborted) {
// Do Something Else
setState(withErrorSomething);
}
}
})();
return () => {
// *** Abort the signal on cleanup
controller.abort();
};
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
...or similar if it doesn't support AbortSignal specifically but does provide some other way of cancelling its work.
If it doesn't, you could fall back to a flag telling you not to use the result:
useEffect(() => {
// *** Start with a flag set to `false`
let cancelled = false;
(async () => {
try {
const workspace = await getFirstWorkspace();
// *** Only do something if the flag is still `false`
if (!cancelled) {
// Do Something
setState(withSomething);
}
} catch (error) {
// *** Only do something if the flag is still `false`
if (!cancelled) {
// Do Something Else
setState(withErrorSomething);
}
}
})();
return () => {
// *** Set the flag on cleanup
cancelled = true;
};
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
It's better to actually cancel the work if you can, but it's fine to have a fallback boolean if you can't. Just don't assume you can't, be sure to check first. :-)
Side note: I love async/await, but when you're doing just a single call and getting a promise, doing an async wrapper and try/catch around await can be a bit overkill. FWIW, just using the promise directly looks like this (using the flag in this case, but it works just as well with the controller/signal):
useEffect(() => {
let cancelled = false;
getFirstWorkspace().then(
(workspace) => {
if (!cancelled) {
// Do Something
setState(withSomething);
}
},
(error) => {
if (!cancelled) {
// Do Something Else
setState(withErrorSomething);
}
}
);
return () => {
cancelled = true;
};
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
U could make use of AbortController
const [state, setState] = useState(undefined);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
(async () => {
try {
const workspace = await getFirstWorkspace(signal);
// Do Something
setState(withSomething)
} catch (error) {
// Do Something Else
setState(withErrorSomething)
}
})();
return =()=>Controller.abort();
}, [generateLink, getFirstWorkspace, masterDataStoreId]);

Proper way to handle a page refresh based on a redux request change

I have created a redux that is going to request an API and if the result is 200, I want to redirect the user to another page using history.
The problem is: I don't know how to trigger this change if the action is a success.
I could redirect the user in my useCase function but I can't use history.push pathName/state argument because it only works in a React component.
So this is what I have done in my React component:
const acceptProposalHandler = () => {
store.dispatch(acceptProposal(id)).then(() => {
setTimeout(() => {
if (isAccepted) { //isAccepted is false by default but is changed to true if the
//request is 200
history.push({
pathname: urls.proposal,
state: {
starterTab: formatMessage({id: 'proposalList.tabs.negotiation'}),
},
});
}
}, 3000);
});
};
Sometimes it works but other times it wont. For some reason, .then is called even if the request fails.
I'm using setTimeOut because if I don't, it will just skip the if statement because the redux hasn't updated the state with isAccepted yet.
This is my useCase function from redux:
export const acceptProposal = (id: string) => async (
dispatch: Dispatch<any>,
getState: () => RootState,
) => {
const {auth} = getState();
const data = {
proposalId: id,
};
dispatch(actions.acceptProposal());
try {
await API.put(`/propostas/change-proposal-status/`, data, {
headers: {
version: 'v1',
'Content-Type': 'application/json',
},
});
dispatch(actions.acceptProposalSuccess());
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
}
};
What I'm doing wrong? I'm using Redux with thunk but I'm not familiar with it.
".then is called even if the request fails." <- this is because acceptProposal is catching the API error and not re-throwing it. If an async function does not throw an error, it will resolve (i.e. call the .then). It can re-throw the error so callers will see an error:
export const acceptProposal = (id: string) => async (
// ... other code hidden
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
// ADD: re-throw the error so the caller can use `.catch` or `try/catch`
throw error;
}
};

React : Updating and then accessing state after a network request in useEffect hook. State remains stale

Im trying to update and reference hasError state field inside of the initialization function of my component in order to control if a redirect happens after successful initialization or if error gets displayed.
Here is a condensed version of the issue:
const [hasError, setHasError] = useState(false);
useEffect(() => {
initialize();
}, []);
async function initialize(){
try {
await networkRequest();
} catch (err) {
setHasError(true);
}
console.log(hasError); // <- is still false
if(!hasError){
redirect() // <- causes redirect even with error
}
}
function networkRequest() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject();
}, 1000);
});
}
The initialization function should only be called once on component mount which is why I'm passing [] to useEffect. Passing [hasError] to useEffect also doesn't make sense since I don't want initialization to run everytime hasError updates.
I have seen people recommend using useReducer but that seems hacky since I'm already using Redux on this component and I'll need to use 2 different dispatch instances then.
How is a use case like this typically handled?
You will have to create another useEffect hook that "listens" for changes to hasError. setHasError in this case is asynchronous, so the new value won't be available to you immediately.
I don't know what the rest of your component looks like, but it sounds like you should have some sort of isLoading state that will be used to display a loading message, then once your request finishes and it fails, you render what you need, or if it succeeds, you redirect.
Here's an example:
function App() {
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
useEffect(() => {
(async () => {
try {
await networkRequest();
isLoading(false);
} catch (error) {
setHasError(error);
isLoading(false);
}
})()
}, [])
useEffect(() => {
if (!isLoading && !hasError) {
redirect();
}
}, [isLoading, hasError]);
if (isLoading) { return "Loading"; }
// else, do whatever you need to do here
}

Can't perform a React state update on an unmounted component with useEffect hook

I have
useEffect(() => {
setLoading(true);
axios
.get(url, {params})
.then(data => {
setData(data || []);
setLoading(false);
})
.catch(() => {
showToast('Load data failed!', 'error');
setLoading(false);
});
}, [params]);
It gives me
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Ok, the question IS NOT HOW TO SOLVE IT. When I use setLoading(false) after axios promise it works fine but inside of promise (e.g. above) it always gives me warning. Actually I want to know WHY IT HAPPENS SO? Is there anybody who may explain me in a nutshell a flow of code above (the process how code above works with warning) and maybe give some best practices on using hooks.
you need clean up function.
this means you should call function end of useEffect function.
when dependencie is changes (params as your example ) calls that function.
so we would be able controll when component mounts/unmounts
useEffect(() => {
let cancelled = false;
setLoading(false);
async function fetchData() {
try {
const response = await axios.get(url, { params });
if (!cancelled) {
setData(response.data);
setLoading(false);
}
} catch (e) {
if (!cancelled) {
showToast(e.message, "error");
setLoading(false);
}
}
}
fetchData();
// clean up here
return () => {
cancelled = true;
};
}, [params]);
WHY IT HAPPENS SO?
Imagine your request is goes slow, and the component has already unmounted when the async request finishes. this time throws this warning

Cancel async fetch request in React Native using Expo

I have a React Native app that has been built using Expo (v35.0.0). I have a simple async function (loadData()) that runs a fetch (via fetchData()) request to an API that the response is then passed into my redux store:
const loadData = async (id, token) => {
setIsLoading(true);
try {
await dispatch(fetchData(id, token));
} catch (error) {
setHasError(true);
}
setIsLoading(false);
};
useEffect(() => {
loadData(user.client.id, user.token);
}, [user.client.id]);
However, when the user logs out we are presented with the warning: "Can't perform a React state update on an unmounted component", which I understand is because the async request has been cancelled.
I have attempted to implement the AbortController approach (as outlined in this article: https://dev.to/iquirino/react-hook-clean-up-useeffect-24e7), however then we are presented with an error stating AbortConroller is unknown.
I thought that support for AbortConroller would be within Expo now as it was added to React Native back in July last year as part of the v0.60 release.
So, is it possible to implement AbortController within Expo to cancel the async request? If not, how should I go about cancelling the request to avoid the warning (& memory leak)?
Because your dispatch is async, it's possible for it to finish after your component is unmounted.
Inside useEffect you can return a function that you call to cleanup. Here you can set a flag to indicate the component is no longer mounted, and if this flag indicates it's no longer mounted, you can tell it not to update state.
eg.
let mounted = true;
const loadData = async (id, token) => {
setIsLoading(true);
try {
await dispatch(fetchData(id, token));
} catch (error) {
if (mounted) setHasError(true);
}
if (mounted) setIsLoading(false);
};
useEffect(() => {
loadData(user.client.id, user.token);
return () => mounted = false;
}, [user.client.id]);

Categories