How to cleanup async tasks created outside useEffect - reactjs

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

Related

How to properly unmount a form in useEffect cleanup function [duplicate]

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

Unmounted component - React Native

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

Async useEffect does not execute entire body when dependency changes

I'm trying to use useEffect with some async functions, however when the dependency in the dependency array changes, only the non async code gets called in useEffect.
useEffect( async () => {
console.log(account);
const web3 = await getWeb3();
const acc = await loadAcc(web3);
await loadContract(web3, acc);
}, [account])
When my account state variable changes, useEffect gets invoked again, but only the console.log(account) statement will get executed.
How should I work around this problem?
The function passed into useEffect cannot be async. You can define an async function inside the useEffect and then use the then/catch syntax.
Further, also pass in all the functions that are defined outside of the useEffect (that you are calling) as a dependency to useEffect
useEffect(() => {
const myAsyncFunc = async () => {
console.log(account);
const web3 = await getWeb3();
const acc = await loadAcc(web3);
await loadContract(web3, acc);
}
myAsyncFunc.catch(console.error);
}, [account, getWeb3, loadAcc, loadContract])
useEffect expected to return either void or a function( the cleanup function ). When you make the function you pass to useEffect as an async, the function will return a promise.
One way to do it is,
useEffect( () => {
const init = async () => {
const web3 = await getWeb3();
const acc = await loadAcc(web3);
const res = await loadContract(web3, acc);
// do something after the async req
}
init();
}, [getWeb3, loadAcc, loadContract])
Or else,
const [web3, setWeb3] = useState(null);
const [acc, setAcc] = useState(null);
useEffect(() => {
getWeb3();
}, [getWeb3])
useEffect(() => {
if (!web3) return;
loadAcc(web3);
}, [web3, loadAcc])
useEffect(() => {
if (acc && web3) {
loadContract(acc, web3);
}
}, [acc, web3, loadContract])
const getWeb3 = useCallback(async () => {
// do some async work
const web3 = // async call
setWeb3(web3)
}, [])
const loadAcc = useCallback(async (web3) => {
// do some async work
const acc = // async call
setAcc(acc);
}, [])
const loadContract = useCallback(async (acc, web3) {
// do anything
}, [])

Calling a custom hook from an onClick by passing it arguments

I created a custom hook to make my api calls with axios.
When I call this hook passing it different parameters, it returns 3 states.
I created a page with a form.
When I submit this form I call a function "onSubmitform"
I would like to be able to execute this custom hook in this function.
How can I do ?
Maybe a custom hook is not suitable in this case?
-- file useAxios.js --
import { useState, useEffect } from "react";
import axios from "axios";
const useAxios = (axiosParams) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const handleData = async (params) => {
try {
const result = await axios.request(params);
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
handleData(axiosParams);
}, []);
return { response, error, loading };
};
export default useAxios;
-- file Page.js --
import useAxios from "../hooks/useAxios";
function Page() {
const { response } = useAxios();
const onSubmitForm = () => {
// Here I want to call the custom hook by passing it different parameters.
}
}
You can add an option to execute the request manually and avoid the fetch on mount:
const useAxios = (axiosParams, executeOnMount = true) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const handleData = async (params) => {
try {
const result = await axios.request(params);
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (executeOnMount) handleData(axiosParams);
}, []);
return { response, error, loading, execute: handleData };
};
Then use it:
const { response, execute } = useAxios(undefined, false);
const onSubmitForm = (data) => {
execute(params) // handleData params
}
A hook is a function (returning something or not) which should be called only when the components (re)renders.
Here you want to use it inside a callback responding to an event, which is not the same thing as the component's render.
Maybe you are just looking for a separate, "simple", function? (for example something similar to what you have in your "useEffect")

React native state not updating function

React native state not updating. Location in alert returns empty.
Help me please
function Home({ route, navigation }) {
const [location, setLocation] = useState('');
const _appStart = () => {
_location();
}
const _location = () => {
setLocation("Konum Ekle");
alert(location);
}
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
_appStart();
});
try this
const _location = useCallback(()=>{
setLocation('Hi')
},console.warn(location)
)
instead of
const _location = () => {
setLocation("Konum Ekle");
alert(location);
}
Basically setState is asynchronous. So call your alert in callback of setLocation, so once setLocation is complete your alert will comes in action.

Resources