I have to functions/const to get data from API:
const [isLoadingRoom, setLoadingRoom] = useState(true);
const [isLoadingLobby, setLoadingLobby] = useState(true);
const [rooms, setRooms] = useState([]);
const [lobbies, setLobbies] = useState([]);
const getRooms = async () => {
let isMounted = true;
async function fetchData() {
const response = await fetch(link);
const json = await response.json();
// 👇️ only update state if component is mounted
if (isMounted) {
setRooms(json);
setLoadingRoom(false);
}
}
fetchData();
return () => {
isMounted = false;
}
}
const getLobbies = async () => {
let isMounted = true;
async function fetchData() {
const response = await fetch(link);
const json = await response.json();
// 👇️ only update state if component is mounted
if (isMounted) {
setLobbies(json);
setLoadingLobby(false);
}
}
fetchData();
return () => {
isMounted = false;
}
}
useEffect(() => {
const roomInterval = setInterval(() => {
getRooms();
getLobbies();
}, 5000);
return () => clearInterval(roomInterval);
}, []);
The API gets data every 5 second, but after a while I get this message:
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.
I have tried different approaches to fetch the API with const, functions, async etc. but I get this error message anyway.. Any tips?
useRef rather than normal variable:
const isMountedRef = useRef(true);
useEffect(() => {
const roomInterval = setInterval(() => {
getRooms();
getLobbies();
}, 5000);
return () => {
clearInterval(roomInterval);
isMountedRef.current = false;
};
}, []);
and change check conditions to
if(isMountedRef.current){
// execute setState
}
Hope it helps. feel free for doubts
Related
How to clean up react request in react hooks. I read that in need to enter in my hook AbortController but I don't know how. I using next.js. What are best methods to eliminate this problem ? And I get this warning:
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.
This is my custom hook to fetch data:
import { useState, useEffect, useCallback } from 'react'
import { MOVIE_API_URL, MOVIE_KEY } from '../../config'
export const useMovieDetailsFetch = (movieId) => {
const [state, setState] = useState({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
const fetchData = useCallback(async () => {
setError(false)
setLoading(true)
try {
const movieDetailsEndpoint = `${MOVIE_API_URL}movie/${movieId}?api_key=${MOVIE_KEY}`
const result = await (await fetch(movieDetailsEndpoint)).json()
const creditsEndpoint = `${MOVIE_API_URL}movie/${movieId}/credits?api_key=${MOVIE_KEY}`
const creditsResult = await (await fetch(creditsEndpoint)).json()
// Filtring in crew for directors only
const movieDirectors = creditsResult.crew.filter(
(member) => member.job === 'Director'
)
setState({
...result,
movieDirectors,
actors: creditsResult.cast,
})
} catch (error) {
setError(true)
}
setLoading(false)
}, [movieId])
useEffect(() => {
fetchData()
}, [fetchData])
return [state, loading, error]
}
Using an abort controller, in its rawest form:
const controller = new AbortController();
const { signal } = controller;
...
fetch(url, { signal });
...
// abort
controller.abort();
To abort an in-flight fetch in effect hook
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
fetch(url, { signal });
return () => {
controller.abort(); // abort on unmount for cleanup
};
}, []);
I found this article very informative when I needed to develop a way to cancel fetch requests.
Edit
The signal needs to be added to the fetch requests options object. You can also define the async fetchData function inside the effect (this is normal), so it's all enclosed in the effect hook's callback scope.
export const useMovieDetailsFetch = (movieId) => {
const [state, setState] = useState({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
const fetchData = async () => {
setError(false);
setLoading(true);
try {
const movieDetailsEndpoint = `${MOVIE_API_URL}movie/${movieId}?api_key=${MOVIE_KEY}`;
const result = await (
await fetch(movieDetailsEndpoint, { signal })
).json();
const creditsEndpoint = `${MOVIE_API_URL}movie/${movieId}/credits?api_key=${MOVIE_KEY}`;
const creditsResult = await (
await fetch(creditsEndpoint, { signal })
).json();
// Filtring in crew for directors only
const movieDirectors = creditsResult.crew.filter(
(member) => member.job === 'Director'
);
setState({
...result,
movieDirectors,
actors: creditsResult.cast,
});
} catch (error) {
setError(true);
}
setLoading(false);
}
fetchData();
return () => controller.abort();
}, [movieId]);
return [state, loading, error];
}
I'm new to react and I'm learning how to use useEffect. I encountered this warning in my react app. I tried out some solutions on SO but the warning still remains. Both fetchUser and fetchPosts trigger this warning. Can anyone enlighten me what is the problem and what does the warning mean?
App.js
useEffect(() => {
setLoading(true)
const getUser = async () => {
const userFromServer = await fetchUser()
if (userFromServer) {
setUser(userFromServer)
setLoading(false)
} else {
console.log("error")
}
}
getUser()
}, [userId])
useEffect(() => {
const getPosts = async () => {
const postsFromServer = await fetchPosts()
setPosts(postsFromServer)
}
getPosts()
}, [userId])
useEffect(() => {
const getUserList = async () => {
const userListFromServer = await fetchUserList()
setUserList(userListFromServer)
}
getUserList()
}, [])
// Fetch user
const fetchUser = async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
const data = await res.json()
return data
}
// Fetch posts
const fetchPosts = async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
const data = await res.json()
return data
}
// Fetch list of users
const fetchUserList = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users/')
const data = await res.json()
return data
}
If you are using any function or state which has been declared outside the useEffect then you need to pass it in the dependency array like this:
const someFunctionA = () => {
....
}
const someFunctionB = () => {
....
}
useEffect(() => {
....
}, [someFunctionA, someFunctionB])
You can read more about it here in case you want to know how it will be rendered: React useEffect - passing a function in the dependency array
I recently used hooks with React to fetch data from server but i'm facing a problem with hooks. The code seems correct but it look like the useEffect isn't called at first time but 3 seconds after with the setInterval. I have blank table for 3 seconds before it appear. I want to directly show the data and call it 3 seconds later.
What is the correct way to use it ?
const [datas, setDatas] = useState([] as any);
useEffect(() => {
const id = setInterval(() => {
const fetchData = async () => {
try {
const res = await fetch(URL);
const json = await res.json();
setDatas(jsonData(json));
} catch (error) {
console.log(error);
}
};
fetchData();
}, TIME)
return () => clearInterval(id);
}, [])
You need to invoke fetchData once initially outside the interval. Define fetchData outside the interval.
useEffect(() => {
// (1) define within effect callback scope
const fetchData = async () => {
try {
const res = await fetch(URL);
const json = await res.json();
setDatas(jsonData(json));
} catch (error) {
console.log(error);
}
};
const id = setInterval(() => {
fetchData(); // <-- (3) invoke in interval callback
}, TIME);
fetchData(); // <-- (2) invoke on mount
return () => clearInterval(id);
}, [])
With React Hooks:
const [seconds, setSeconds] = useState(0)
const interval = useRef(null)
useEffect(() => { if (seconds === 60) stopCounter() }, [seconds])
const startCounter = () => interval.current = setInterval(() => {
setSeconds(prevState => prevState + 1)
}, 1000)
const stopCounter = () => clearInterval(interval.current)
I am trying to load a user if the token in localStorage is found. Of course, nowhere in my code am I assigning true to isAuthenticated but my question more so is if I do the following...
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
let mounted = true;
setIsAuthenticated(false);
const loadUser = async () => {
await fetch('http://localhost:5000/api/auth', {
method: 'GET',
headers: {
'x-auth-token': localStorage.token,
},
});
if (!mounted) {
setIsAuthenticated(false);
}
};
loadUser();
return () => {
mounted = false;
};
}, []);
I do not get this issue...
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.
However, if I were to change the
if (!mounted) {
setIsAuthenticated(false);
}
to
if (!mounted) {
setIsAuthenticated(true);
}
I do get that warning and I'm just wondering why this happens so that I know how to deal with it properly whenever it comes up.
The warning is:
Warning: Can't perform a React state update on an unmounted component.
You can call the state setter but you can't change the state because that will cause a re render but React can't re render because the component has been unmounted, see code below:
const Working = () => {
const [value, setValue] = React.useState(true);
React.useEffect(() => {
//not changing the value
setTimeout(() => setValue(true), 100);
});
return `value is: ${value}`;
};
const Warning = () => {
const [value, setValue] = React.useState(true);
React.useEffect(() => {
//changing the value causes re render
// but component was unmounted by App
setTimeout(() => setValue(false), 100);
});
return `value is: ${value}`;
};
const App = () => {
const [remount, setRemount] = React.useState(true);
const causeWarning = () => {
setRemount(false);
Promise.resolve(true)
.then(setRemount)
.then(() => setRemount(false));
};
return (
<div>
<button onClick={causeWarning}>cause warnig</button>
{remount && <Working />}
{remount && <Warning />}
</div>
);
};
It's best to not call the state setter at all when the component was unmounted:
if (mounted) {
setIsAuthenticated(false);
}
The part where the warning says:
This is a no-op, but it indicates a memory leak in your application.
Means that you may
you have to do like this
useEffect(() => {
let mounted = true;
setIsAuthenticated(false);
const loadUser = async () => {
await fetch('http://localhost:5000/api/auth', {
method: 'GET',
headers: {
'x-auth-token': localStorage.token,
},
}).then(()=> {
if (!mounted) {
setIsAuthenticated(true);
}
});
};
loadUser();
return () => {
mounted = false;
};
}, []);
I created a custom hook useFetch that returns a fetch function that I can use in other components. It uses a promise to fetch some data inside. My goal is to clean up the pending promise, if the component, that uses this custom hook gets unmounted.
How would I do it? I tried something using useRef, but without success yet. Still getting the Can't perform a React state update on an unmounted component. warning.
const useFetch = (url) => {
const [isFetching, setIsFetching] = useState(false)
const handler = useRef(null)
useEffect(() => () => {
if (handler.current !== null) {
handler.current.cancel()
}
}, [])
return (options) => {
handler.current = window.fetch(url, options)
setIsFetching(true)
return handler.current.then(() => {
handler.current = null
setIsFetching(false)
})
}
}
export default () => {
const fetchData = useFetch('www.tld')
useEffect(() => {
fetchData({}).then(() => console.log('done'))
}, [])
return null
}
Notice that the promise in this example is cancelable via .cancel() (so thats not a problem here).
Return cancel() as bound callback from your hook. Then it would be up to consumer to stop it:
const useFetch(url) {
const [isFetching, setIsFetching] = useState(false)
const handler = useRef(null)
function run(options) {
handler.current = window.fetch(url, options)
setIsFetching(true)
...
}
function cancel() {
if(handler.current) {
handler.current.cancel()
}
}
return {run, cancel}
}
...
function OtherComponent({userId}) {
const [userData, setUserData] = useState(null);
const {run, cancel} = useFetch(`/user/${userId}`);
useEffect(() => {
run(options).then(setUserData);
return cancel; // it's up to consumer code to stop request
}, [userId]);
}