UseEffect is not running cleanup on-dismount - reactjs

I have a MessageInput component which triggers a socket connection to the backend when it mounts, and should disconnect from that socket manually on dismount. I am not sure what happened, but at some point yesterday useEffect simply stopped running cleanup.
When I close the app I see the accumulated disconnections all at once.
User disconnected...
User disconnected...
User disconnected...
Is there something I am missing in useEffect?
Here is my code:
useEffect(async () => {
const accessToken = await SecureStore.getItemAsync("access-token");
const socket = socketClient(`${process.env.REACT_APP_backend_url}/`, {
auth: {
authToken: accessToken,
},
});
socket.emit("message", message);
return () => socket.disconnect();
}, []);

async functions return promises, so you're not actually returning a cleanup function. React ignores the promise that you return to it.
You'll need to do something like this:
useEffect(() => {
let socket;
(async () => {
const accessToken = await SecureStore.getItemAsync("access-token");
socket = socketClient(`${process.env.REACT_APP_backend_url}/`, {
auth: {
authToken: accessToken,
},
});
socket.emit("message", message);
})()
return () => {
if (socket) {
socket.disconnect();
}
}
}, []);

Related

React Error: Effect callbacks are synchronous to prevent race conditions. Put the async function inside" [duplicate]

I was trying the useEffect example something like below:
useEffect(async () => {
try {
const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
const json = await response.json();
setPosts(json.data.children.map(it => it.data));
} catch (e) {
console.error(e);
}
}, []);
and I get this warning in my console. But the cleanup is optional for async calls I think. I am not sure why I get this warning. Linking sandbox for examples. https://codesandbox.io/s/24rj871r0p
For React version <=17
I suggest to look at Dan Abramov (one of the React core maintainers) answer here:
I think you're making it more complicated than it needs to be.
function Example() {
const [data, dataSet] = useState<any>(null)
useEffect(() => {
async function fetchMyAPI() {
let response = await fetch('api/data')
response = await response.json()
dataSet(response)
}
fetchMyAPI()
}, [])
return <div>{JSON.stringify(data)}</div>
}
Longer term we'll discourage this pattern because it encourages race conditions. Such as — anything could happen between your call starts and ends, and you could have gotten new props. Instead, we'll recommend Suspense for data fetching which will look more like
const response = MyAPIResource.read();
and no effects. But in the meantime you can move the async stuff to a separate function and call it.
You can read more about experimental suspense here.
If you want to use functions outside with eslint.
function OutsideUsageExample({ userId }) {
const [data, dataSet] = useState<any>(null)
const fetchMyAPI = useCallback(async () => {
let response = await fetch('api/data/' + userId)
response = await response.json()
dataSet(response)
}, [userId]) // if userId changes, useEffect will run again
useEffect(() => {
fetchMyAPI()
}, [fetchMyAPI])
return (
<div>
<div>data: {JSON.stringify(data)}</div>
<div>
<button onClick={fetchMyAPI}>manual fetch</button>
</div>
</div>
)
}
For React version >=18
Starting with React 18 you can also use Suspense, but it's not yet recommended if you are not using frameworks that correctly implement it:
In React 18, you can start using Suspense for data fetching in opinionated frameworks like Relay, Next.js, Hydrogen, or Remix. Ad hoc data fetching with Suspense is technically possible, but still not recommended as a general strategy.
If not part of the framework, you can try some libs that implement it like swr.
Oversimplified example of how suspense works. You need to throw a promise for Suspense to catch it, show fallback component first and render Main component when promise it's resolved.
let fullfilled = false;
let promise;
const fetchData = () => {
if (!fullfilled) {
if (!promise) {
promise = new Promise(async (resolve) => {
const res = await fetch('api/data')
const data = await res.json()
fullfilled = true
resolve(data)
});
}
throw promise
}
};
const Main = () => {
fetchData();
return <div>Loaded</div>;
};
const App = () => (
<Suspense fallback={"Loading..."}>
<Main />
</Suspense>
);
When you use an async function like
async () => {
try {
const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
const json = await response.json();
setPosts(json.data.children.map(it => it.data));
} catch (e) {
console.error(e);
}
}
it returns a promise and useEffect doesn't expect the callback function to return Promise, rather it expects that nothing is returned or a function is returned.
As a workaround for the warning you can use a self invoking async function.
useEffect(() => {
(async function() {
try {
const response = await fetch(
`https://www.reddit.com/r/${subreddit}.json`
);
const json = await response.json();
setPosts(json.data.children.map(it => it.data));
} catch (e) {
console.error(e);
}
})();
}, []);
or to make it more cleaner you could define a function and then call it
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(
`https://www.reddit.com/r/${subreddit}.json`
);
const json = await response.json();
setPosts(json.data.children.map(it => it.data));
} catch (e) {
console.error(e);
}
};
fetchData();
}, []);
the second solution will make it easier to read and will help you write code to cancel previous requests if a new one is fired or save the latest request response in state
Working codesandbox
Until React provides a better way, you can create a helper, useEffectAsync.js:
import { useEffect } from 'react';
export default function useEffectAsync(effect, inputs) {
useEffect(() => {
effect();
}, inputs);
}
Now you can pass an async function:
useEffectAsync(async () => {
const items = await fetchSomeItems();
console.log(items);
}, []);
Update
If you choose this approach, note that it's bad form. I resort to this when I know it's safe, but it's always bad form and haphazard.
Suspense for Data Fetching, which is still experimental, will solve some of the cases.
In other cases, you can model the async results as events so that you can add or remove a listener based on the component life cycle.
Or you can model the async results as an Observable so that you can subscribe and unsubscribe based on the component life cycle.
You can also use IIFE format as well to keep things short
function Example() {
const [data, dataSet] = useState<any>(null)
useEffect(() => {
(async () => {
let response = await fetch('api/data')
response = await response.json()
dataSet(response);
})();
}, [])
return <div>{JSON.stringify(data)}</div>
}
void operator could be used here.
Instead of:
React.useEffect(() => {
async function fetchData() {
}
fetchData();
}, []);
or
React.useEffect(() => {
(async function fetchData() {
})()
}, []);
you could write:
React.useEffect(() => {
void async function fetchData() {
}();
}, []);
It is a little bit cleaner and prettier.
Async effects could cause memory leaks so it is important to perform cleanup on component unmount. In case of fetch this could look like this:
function App() {
const [ data, setData ] = React.useState([]);
React.useEffect(() => {
const abortController = new AbortController();
void async function fetchData() {
try {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const response = await fetch(url, { signal: abortController.signal });
setData(await response.json());
} catch (error) {
console.log('error', error);
}
}();
return () => {
abortController.abort(); // cancel pending fetch request on component unmount
};
}, []);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
I read through this question, and feel the best way to implement useEffect is not mentioned in the answers.
Let's say you have a network call, and would like to do something once you have the response.
For the sake of simplicity, let's store the network response in a state variable.
One might want to use action/reducer to update the store with the network response.
const [data, setData] = useState(null);
/* This would be called on initial page load */
useEffect(()=>{
fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(data => {
setData(data);
})
.catch(err => {
/* perform error handling if desired */
});
}, [])
/* This would be called when store/state data is updated */
useEffect(()=>{
if (data) {
setPosts(data.children.map(it => {
/* do what you want */
}));
}
}, [data]);
Reference => https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
For other readers, the error can come from the fact that there is no brackets wrapping the async function:
Considering the async function initData
async function initData() {
}
This code will lead to your error:
useEffect(() => initData(), []);
But this one, won't:
useEffect(() => { initData(); }, []);
(Notice the brackets around initData()
For fetching from an external API using React Hooks, you should call a function that fetches from the API inside of the useEffect hook.
Like this:
async function fetchData() {
const res = await fetch("https://swapi.co/api/planets/4/");
res
.json()
.then(res => setPosts(res))
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
}, []);
I strongly recommend that you do not define your query inside the useEffect Hook, because it will be re-render infinite times. And since you cannot make the useEffect async, you can make the function inside of it to be async.
In the example shown above, the API call is in another separated async function so it makes sure that the call is async and that it only happens once. Also, the useEffect's dependency array (the []) is empty, which means that it will behave just like the componentDidMount from React Class Components, it will only be executed once when the component is mounted.
For the loading text, you can use React's conditional rendering to validate if your posts are null, if they are, render a loading text, else, show the posts. The else will be true when you finish fetching data from the API and the posts are not null.
{posts === null ? <p> Loading... </p>
: posts.map((post) => (
<Link key={post._id} to={`/blog/${post.slug.current}`}>
<img src={post.mainImage.asset.url} alt={post.mainImage.alt} />
<h2>{post.title}</h2>
</Link>
))}
I see you already are using conditional rendering so I recommend you dive more into it, especially for validating if an object is null or not!
I recommend you read the following articles in case you need more information about consuming an API using Hooks.
https://betterprogramming.pub/how-to-fetch-data-from-an-api-with-react-hooks-9e7202b8afcd
https://reactjs.org/docs/conditional-rendering.html
try
const MyFunctionnalComponent: React.FC = props => {
useEffect(() => {
// Using an IIFE
(async function anyNameFunction() {
await loadContent();
})();
}, []);
return <div></div>;
};
Other answers have been given by many examples and are clearly explained, so I will explain them from the point of view of TypeScript type definition.
The useEffect hook TypeScript signature:
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
The type of effect:
// NOTE: callbacks are _only_ allowed to return either void, or a destructor.
type EffectCallback = () => (void | Destructor);
// Destructors are only allowed to return void.
type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };
Now we should know why effect can't be an async function.
useEffect(async () => {
//...
}, [])
The async function will return a JS promise with an implicit undefined value. This is not the expectation of useEffect.
Please try this
useEffect(() => {
(async () => {
const products = await api.index()
setFilteredProducts(products)
setProducts(products)
})()
}, [])
To do it properly and avoid errors: "Warning: Can't perform a React state update on an unmounted..."
useEffect(() => {
let mounted = true;
(async () => {
try {
const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
const json = await response.json();
const newPosts = json.data.children.map(it => it.data);
if (mounted) {
setPosts(newPosts);
}
} catch (e) {
console.error(e);
}
})();
return () => {
mounted = false;
};
}, []);
OR External functions and using an object
useEffect(() => {
let status = { mounted: true };
query(status);
return () => {
status.mounted = false;
};
}, []);
const query = async (status: { mounted: boolean }) => {
try {
const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
const json = await response.json();
const newPosts = json.data.children.map(it => it.data);
if (status.mounted) {
setPosts(newPosts);
}
} catch (e) {
console.error(e);
}
};
OR AbortController
useEffect(() => {
const abortController = new AbortController();
(async () => {
try {
const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`, { signal: abortController.signal });
const json = await response.json();
const newPosts = json.data.children.map(it => it.data);
setPosts(newPosts);
} catch (e) {
if(!abortController.signal.aborted){
console.error(e);
}
}
})();
return () => {
abortController.abort();
};
}, []);
I know it is late but just I had the same problem and I wanted to share that I solved it with a function like this!
useEffect(() => {
(async () => {
try {
const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
const json = await response.json();
setPosts(json.data.children.map(it => it.data));
} catch (e) {
console.error(e);
}
}) ()
}, [])
With useAsyncEffect hook provided by a custom library, safely execution of async code and making requests inside effects become trivially since it makes your code auto-cancellable (this is just one thing from the feature list). Check out the Live Demo with JSON fetching
import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpFetch from "cp-fetch";
/*
Notice: the related network request will also be aborted
Checkout your network console
*/
function TestComponent(props) {
const [cancel, done, result, err] = useAsyncEffect(
function* () {
const response = yield cpFetch(props.url).timeout(props.timeout);
return yield response.json();
},
{ states: true, deps: [props.url] }
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>
{done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
</div>
<button className="btn btn-warning" onClick={cancel} disabled={done}>
Cancel async effect
</button>
</div>
);
}
export default TestComponent;
The same demo using axios
Just a note about HOW AWESOME the purescript language handles this problem of stale effects with Aff monad
WITHOUT PURESCRIPT
you have to use AbortController
function App() {
const [ data, setData ] = React.useState([]);
React.useEffect(() => {
const abortController = new AbortController();
void async function fetchData() {
try {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const response = await fetch(url, { signal: abortController.signal });
setData(await response.json());
} catch (error) {
console.log('error', error);
}
}();
return () => {
abortController.abort(); // cancel pending fetch request on component unmount
};
}, []);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
or stale (from NoahZinsmeister/web3-react example)
function Balance() {
const { account, library, chainId } = useWeb3React()
const [balance, setBalance] = React.useState()
React.useEffect((): any => {
if (!!account && !!library) {
let stale = false
library
.getBalance(account)
.then((balance: any) => {
if (!stale) {
setBalance(balance)
}
})
.catch(() => {
if (!stale) {
setBalance(null)
}
})
return () => { // NOTE: will be called every time deps changes
stale = true
setBalance(undefined)
}
}
}, [account, library, chainId]) // ensures refresh if referential identity of library doesn't change across chainIds
...
WITH PURESCRIPT
check how useAff kills it's Aff in the cleanup function
the Aff is implemented as a state machine (without promises)
but what is relevant to us here is that:
the Aff encodes how to stop the Aff - You can put your AbortController here
it will STOP running Effects (not tested) and Affs (it will not run then from the second example, so it will NOT setBalance(balance)) IF the error was thrown TO the fiber OR INSIDE the fiber
Ignore the warning, and use the useEffect hook with an async function like this:
import { useEffect, useState } from "react";
function MyComponent({ objId }) {
const [data, setData] = useState();
useEffect(() => {
if (objId === null || objId === undefined) {
return;
}
async function retrieveObjectData() {
const response = await fetch(`path/to/api/objects/${objId}/`);
const jsonData = response.json();
setData(jsonData);
}
retrieveObjectData();
}, [objId]);
if (objId === null || objId === undefined) {
return (<span>Object ID needs to be set</span>);
}
if (data) {
return (<span>Object ID is {objId}, data is {data}</span>);
}
return (<span>Loading...</span>);
}
The most easy way is to use useAsyncEffect from 'use-async-effect'
You can find it on NPM.
const ProtectedRoute = ({ children }) => {
const [isAuth, setIsAuth] = useState(false);
useAsyncEffect(async () => {
try {
const data = await axios("auth");
console.log(data);
setIsAuth(true);
} catch (error) {
console.log(error);
}
}, []);
if (!isAuth)
return <Navigate to="/signin" />
return children;
}

Connection leak with a Socket.IO component in React

I have a React application that uses Socket.IO. The Socket instance is in a React component.
I have been noticing that the action of logging out and logging into my application, which should unmount the component and close the connection and then remount and reopen the connection leads to a socket leak/creation of duplicate socket connection. I have also managed to get the application into a state where it quickly spews off new connections leading to starvation, but have not been able to replicate. This hit production once.
Here is the client code:
const Socket = React.memo(() => {
const [isLoadingSocket, setIsLoadingSocket] = useState<boolean>(false)
const socketRef = useRef<SocketIO<ServerToClientEvents, ClientToServerEvents> | null>(null)
const socketNeedsRestart = isFocused ? !isLoadingSocket : false
async function initializeSocket() {
const token = await getToken()
setIsLoadingSocket(true)
if (socketRef.current) {
socketRef.current.disconnect()
}
socketRef.current = io(`${SOCKET_HOST}`, {
secure: true,
reconnectionDelay: 5000,
transports: ['websocket', 'polling'],
path: ENVIRONMENT !== Environment.local ? '/api/socket.io/' : '',
auth: {
token,
},
})
console.log(`socket initialized`)
}
useEffect(() => {
if (socketNeedsRestart) {
initializeSocket()
}
}, [socketNeedsRestart]) //eslint-disable-line
useEffect(() => {
if (socketRef.current) {
socketRef.current.on(SocketLifecycleEvent.Connect, () => {
console.log('socket connected')
setIsLoadingSocket(false)
})
socketRef.current.on(SocketMessage.UsersOnline, (message) => {
updateOnlineUsers(message.onlineUserIDs)
})
}
return () => {
if (socketRef.current) {
socketRef.current.off(SocketLifecycleEvent.Connect)
socketRef.current.off(SocketLifecycleEvent.ConnectionError)
socketRef.current.off(SocketLifecycleEvent.Disconnect)
}
}
}, [isLoadingSocket])
useEffect(() => {
socketRef.current?.disconnect()
}, [])
return <></>
})
export default Socket
The component is used once in the page that a user gets to after login. I can provide server code but it doesn't do anything except notify all users every time someone connects. What's causing the connection leak? How can I re-create the rapid-fire leak?
Is your last useEffect doing what you expect it to do? It looks like this is supposed to be the cleanup on unmount but you are not returning a function there.
Did you try something like this?:
useEffect( () => () => socketRef.current?.disconnect(), [] );

How to integrate AbortController with Axios and React?

The Abortcontroller signal is not working for me with Axios in React.
I wanted to replace CancelToken (as it's deprecated) with the AbortController, but it is not working, respectively the requests are not being canceled.
let testController: AbortController;
function loadTest() {
testController = new AbortController();
TestAPI.getTest(testController.signal)
.then((e) => {
console.log(e.data);
})
.catch((e) => {
console.error(e);
});
}
Also in the UseEffect Cleanup I do this (here it should cancel) and also the signal's state is set to aborted, but still the request is not canceled:
useEffect(() => () => {
if (testController) testController.abort();
// console.log(testController.signal.aborted) => **true**
}, []);
Here is my API, where I pass the AbortSignal to the request:
getTest(signal?: AbortSignal): Promise<AxiosResponse<Test[]>> {
return axios.get(`${URI}/test`, { signal });
},
When using Axios.CancelToken.source was working fine, but now with the AbortController, the request is never canceled.
Using: "axios": "^0.26.0",
Did someone manage to integrate the AbortController with React and Axios? Or does the AbortController only work with fetch?
The axios.CancelToken API isn't deprecated as far as I can tell, it's still in the spec, but according to the docs axios also supports the AbortController of the fetch API.
Cancellation
Axios supports AbortController to abort requests in fetch API way:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
It's not clear exactly where testController is declared:
let testController: AbortController;
but I suspect it's in the body of the function component and redeclared on a subsequent render cycle.
I suggest using a React ref to store an AbortController, and reference this ref value around your app. This is so the component holds on to a stable reference of the controller from render cycle to render cycle, to be referenced in any useEffect hook cleanup function to cancel in-flight requests if/when the component unmounts.
const abortControllerRef = useRef<AbortController>(new AbortController());
function loadTest() {
TestAPI.getTest(abortControllerRef.current.signal)
.then((e) => {
console.log(e.data);
})
.catch((e) => {
console.error(e);
});
}
useEffect(() => {
const controller = abortControllerRef.current;
return () => {
controller.abort();
};
}, []);
I would recommend to read this post.
In a nutshell you would like to use useEffect to create controller, and, what is more important, to use return statement to abort the controller.
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
getData(signal)
//cleanup function
return () => {controller.abort();};
}, [fetchClick]);
getData function can then be your axious call in the form:
const getData = async (signal) =>{
const res = await axios.get(url, {signal: signal}).then(...)
}
Abort controller often use in useEffect to fetch some data. So, in order to implement the control you can try this:
//...
const [data, setData] = useState([]);
useEffect(() => {
const controller = new AbortController();
axios
.get("https://somedata.com", { signal: controller.signal })
.then(res => {
setData(res.data);
})
.catch(err => console.log(err));
// return cleanup function to abort request
return () => {
controller.abort();
};
}, []);
//...
There's my code example, hope this helps:
useEffect(() => {
const abortController = new AbortController();
const getData = async () => {
try {
const res = await axios("/api/data/", {
signal: abortController.signal,
});
const data = res.data
} catch (error) {
if (error.name !== "CanceledError") {
/* Logic for non-aborted error handling goes here. */
console.log('error:', error)
}
}
};
getData();
// clean up function when unmounted to avoid getData fired twice problem in React 18
return () => abortController.abort();
}, []);
All you need regarding AbortController with axios here
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()

Can't fetch data with Axios and React, getting an Promise and Undefined

I'm trying to fetch some data with Axios and React, But I'm having a problem resolving the promise and setting it on the state, that's weird.
Here is the Base:
export const fetchUserById = (username) => client.get(`/${username}`);
Here is the Call:
export const getUserById = async (username) => {
try {
const response = await api.fetchUserById(username);
const data = await response.data;
return data;
} catch (error) {
return error;
}
};
Here is in React:
const [user, setUser] = useState();
useEffect(() => {
const data = getUserById(params.username); // this gets the username and its working
setUser(data)
}, [])
useEffect(() => {
console.log("this is user: ", user)
}, [user])
If I console log user, I get undefined, If I console log data i get a promise.
getUserById is declared async so it implicitly returns a Promise that callers should either await or use a Promise chain on.
useEffect(() => {
const data = getUserById(params.username);
setUser(data); // <-- logs only the returned Promise object!
}, [])
async/await
useEffect(() => {
const getUser = async () => {
try {
const data = await getUserById(params.username);
setUser(data);
} catch(error) {
// handle error, log, etc...
}
};
getUser();
}, []);
Promise chain
useEffect(() => {
getUserById(params.username)
.then(data => {
setUser(data);
})
.catch(error => {
// handle error, log, etc...
});
};
}, []);
Or you could as well do:
useEffect(() => {
// fetch data
(async () => {
try {
const data = await getUserById(params.username);
// set state
setUser(data)
} catch(error) {
// handle error, log, etc...
// set init state
setUser(null)
}
})();
}, []);

How can I successfully unsubscribe from user notfiications (firebase, react)

my users have a collection in their userData document called notifications. I want to listen to these in realtime so you can see notifications instantly (like any social media app, facebook instagram etc).
In my app.js, I currently have this useEffect:
useEffect(() => {
//this gets the user data
const unsubscribeUserData = async () => {
const userData = await getUserByUserId(currentUser.uid);
setData(userData);
};
//this subscribes to their notifications
{
/*
//This the unsub method
const unsubscribe = db
.collection("users")
.doc(currentUser.uid)
.collection("notifications")
.orderBy("date", "desc")
.onSnapshot((snapshot) => {
let tempNotifications = [];
snapshot.forEach((notification) => {
console.log("adding notification: ", notification);
tempNotifications.push(notification.data());
});
setNotifications(tempNotifications);
});
*/
}
//What do I do with useRef?
const unsubscribe = useRef();
if (currentUser) {
//If there is an auth object stored in my auth context, get their data and subscribe to their notifications
unsubscribeUserData();
unsubscribe();
} else {
//If there isn't, unsub from the notifications and set data to null
setData(null);
unsubscribe();
}
return () => {
//If they have closed the webpage, unubscribe
unsubscribe();
};
}, [currentUser]);
Can anyone give me a hint as to how to structure this so it unsubscribes successfully when needed?
Thanks!
EDIT (I believe this works!)
useEffect(() => {
const getUserData = async () => {
const userData = await getUserByUserId(currentUser.uid);
setData(userData);
};
if (currentUser) {
getUserData();
return (unsubscribeRef.current = db
.collection("users")
.doc(currentUser.uid)
.collection("notifications")
.orderBy("date", "desc")
.onSnapshot((snapshot) => {
let tempNotifications = [];
snapshot.forEach((notification) => {
console.log("adding notification: ", notification);
tempNotifications.push(notification.data());
});
setNotifications(tempNotifications);
}));
} else {
setData(null);
}
}, [currentUser]);
I believe that calling the return at the actual snapshot listener is the same as called a cleanup return, however doing it here skips the not a function error
The main issue is that useEffect hook callbacks can't be async functions as these implicitly return a Promise which would, by design, be interpreted as an useEffect hook cleanup function. This won't work.
After looking at your code for a bit it looks like getting and setting the data state is independent of the firebase subscription. You can minify just the asynchronous logic into an async function to get and set the user data and invoke that, and access the firebase collection as per normal.
Use a React ref to hold on to the unsubscribe callback.
I think the following implementation should get you very close to what you are looking for. (Disclaimer: I've not tested this code but I think it should work)
const unsubscribeRef = useRef();
useEffect(() => {
const getUserData = async () => {
const userData = await getUserByUserId(currentUser.uid);
setData(userData);
};
if (currentUser) {
getUserData();
unsubscribeRef.current = db
.collection("users")
.doc(currentUser.uid)
.collection("notifications")
.orderBy("date", "desc")
.onSnapshot((snapshot) => {
let tempNotifications = [];
snapshot.forEach((notification) => {
console.log("adding notification: ", notification);
tempNotifications.push(notification.data());
});
setNotifications(tempNotifications);
});
} else {
setData(null);
}
return () => {
// Unsubscribe any current subscriptions before rerender/unmount
// Note: uses Optional Chaining operator, if this can't work for you
// use normal null-check:
// unsubscribeRef.current && unsubscribeRef.current()
unsubscribeRef?.current();
};
}, [currentUser]);

Resources