how to properly cleanup useEffect that contain async graphql operation - javascript

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

Related

React 18 strict mode causing component to render twice

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

React state is empty after setting

I have simple nextjs app where i want to save and print state to fetchData
This is my code
const Room = () => {
const router = useRouter();
const [fetchData, setFetchData] = useState("");
useEffect(() => {
if (router.asPath !== router.route) {
getDataNames();
console.log(fetchData); // Empty
}
}, [router]);
const getDataNames = async () => {
try {
await fetch("http://localhost:1337/rooms?_id=" + router.query.id)
.then((response) => response.json())
.then((jsonData) => setFetchData(jsonData) & console.log(jsonData)); // Data are logged in console
} catch (e) {
console.error(e);
}
};
Problem is that fetchData is empty on console log but jsonData gaves me actual data. And i have no idea what can be problem.
Ok so 3 things here. Firstly, your useEffect doesn't re-run when your fetchData value changes. You need to add it to the dependency list:
useEffect(() => {
if (router.asPath !== router.route) {
getDataNames();
console.log(fetchData); // Empty
}
}, [router, fetchData]);
this way the effect will run when fetchData changes. And of course it will only console.log it if that condition inside the effect's if condition is satisfied.
Secondly, you're mixing async/await and .then/.catch syntax, don't do that. Use one or the other. In this case you can just remove the await keyword and try/catch block, and use your existing .then code, and add a .catch to it to catch any errors.
Finally, & in Javascript is the bitwise AND operator, so not sure why you were using it there. If you think it means "and also do this", then that's incorrect. Just put the .then function inside curly braces and call the setFetchData and console.log statements one after the other:
.then((jsonData) => {
setFetchData(jsonData);
console.log(jsonData)
});
useEffect(() => {
console.log(fetchData);
}, [fetchData]);
use this hook to log your data

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

JestJS: Async test isn't stopped

I got two problems with this jest test:
Is it possible to define the Content collection only once instead of doing that inside of the test?
I do get this error:
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles to troubleshoot this issue.
I don't see why my async code weren't stopped...
import resolvers from 'resolvers/'
import Db from 'lib/db'
const db = new Db()
describe('Resolver', () => {
let token
beforeAll(async () => {
await db.connect()
})
beforeEach(async () => {
token = 'string'
await db.dropDB()
})
afterAll(async () => {
await db.connection.close()
})
describe('articleGetContent()', () => {
test('should return dataset', async () => {
// SETUP
const Content = db.connection.collection('content')
const docs = [{
// some content...
}]
await Content.insertMany(docs)
// EXECUTE
const result = await resolvers.Query.articleGetContent({}, {
id: '123,
language: 'en'
}, {
token
})
// VERIFY
expect.assertions(1)
expect(result).toBeDefined()
})
})
})
resolver
import { articleGetContent } from '../models/article'
export default {
Query: {
articleGetContent: async (obj, { id }, { token }) => articleGetContent(id, token)
}
}
This is how my db class looks like
db.js
export default class Db {
constructor (uri, callback) {
const mongo = process.env.MONGO || 'mongodb://localhost:27017'
this.mongodb = process.env.MONGO_DB || 'testing'
this.gfs = null
this.connection = MongoClient.connect(mongo, { useNewUrlParser: true })
this.connected = false
return this
}
async connect (msg) {
if (!this.connected) {
try {
this.connection = await this.connection
this.connection = this.connection.db(this.mongodb)
this.gfs = new mongo.GridFSBucket(this.connection)
this.connected = true
} catch (err) {
console.error('mongo connection error', err)
}
}
return this
}
async disconnect () {
if (this.connected) {
try {
this.connection = await this.connection.close()
this.connected = false
} catch (err) {
console.error('mongo disconnection error', err)
}
}
}
async dropDB () {
const Content = this.connection.collection('content')
await Content.deleteMany({})
}
}
Related to the second question I hope you've found some issues on github about it.
In general, the issue is described in the debug log.
Jest works with promises, as a result, you shouldn't leave any async operations in any status except resolved.
In your case, you have your DB connection opened so you need to implement another method disconnect for your DB class, this link to docs will help you, but I guess you have it already as it's not the full db.js file ( I see some custom method dropDB. Main idea here is to have it in afterAll hook:
afterAll(() => db.disconnect());
Great example at the bottom of the page
What about the first question, it really depends on what you are doing in your method dropDB. If you're running method for dropping collection, you could store the reference to this collection somewhere outside and use it as it will automatically create the new one, but it would be great to see this method.
Additionally, your async test was created in a wrong way, you could read more here for example in my Update. You need to run this function in the beginning of the test: expect.assertions(number)
expect.assertions(number) verifies that a certain number of assertions
are called during a test. This is often useful when testing
asynchronous code, in order to make sure that assertions in a callback
actually got called.

Categories