Custom react hook causes infinite loop - reactjs

I have a custom fetch hook:
export const useFetch = (url: string) => {
const [response, setResponse] = useState<any>(null);
const [error, setError] = useState<any>(null);
const fetchList = (url: string) => {
return API.get(AMPLIFY_ENPOINTS.default, url, { response: true });
};
useEffect(() => {
const fetchFunc = async () => {
try {
const fetchResponse = await fetchList(url);
setResponse(fetchResponse.data);
} catch (error) {
setError(error);
}
};
fetchFunc();
}, [url]);
return { response, error };
};
This I then use in a component:
const fetchOrders = useFetch(apiUrl);
useEffect(() => {
const { response, error } = fetchOrders;
if (error) setError(error);
if (response) {...}
}, [fetchOrders]);
And this causes an infinite loop, how should I go about fixing it?

The fetchOrder reference keeps changes on each re-render since you are returning a newly created object each time and hence it triggers an infinite loop when you call setError within your useEffect.
Instead of adding fetchOrders as a dependency, add response and error separately
const { response, error } = useFetch(apiUrl);
useEffect(() => {
if (error) setError(error);
if (response) {...}
}, [response, error]);

It is this line that is responsible for this
return { response, error };
And of course his partner in crime is this [fetchOrders] dependency array in the second useEffect.
The first line returns a new object and thus fetchOrders is always a new value.
After thinking about your code, I came up to conclude that you probably don't need the second useEffect at all (I think).
// the useFetch only re-fetches when url changes
const { response, error } = useFetch(url);
// use response directly, you don't need to re-set the error nor the response in your component.
But if you were to do a side effect with response, you can consider upgrading your useFetch hook to accept a callback parameter and useEffect with a response dependency.

In your component, try wrapping the input in useMemo.
const url = useMemo(() => apiUrl, [])
const fetchOrders = useFetch(url);
useEffect(() => {
const { response, error } = fetchOrders;
if (error) setError(error);
if (response) {...}
}, [fetchOrders]);
Otherwise, apiUrl is a local variable and gets recreated every time the functional component runs, causing the infinite loop.

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

unmount state update in useEffect() cleanup function

To get data from API I call a http request. Sometimes I warned with a error that tell me I am trying to update state which unmounted. To solve that, I use clean up function in useEffect() hook like this:
const [products, setProducts] = useState([]);
useEffect(() => {
const source = axios.CancelToken.source();
const token = source.token;
const fetchProducts = async () => {
try {
const response = await ProductService.getProducts(token);
setProducts(response.data);
} catch (error) {
console.log(error.message, error.response.status);
}
};
fetchProducts();
return () => {
source.cancel();
};
}, []);
and my service file like this:
const ProductService = {
getProducts: async function (token) {
try {
const response = await axios.get(myURL, {
cancelToken: token
});
return response.data
} catch (error) {
throw error
}
}
};
Have I done anything wrong or unnecessary thing in this case or can I update this code block ??
Please help me.
I see, so you're in the twilight zone between the asynchronous request succeeding, so the cancel token won't work, and the enqueued state update.
From here you've a couple options.
Ignore since this is only a warning. You've already tried cancelling in-flight network requests, or unsubscribed from subscriptions, etc... so at this point it's only a warning.
Use the old isMounted hack.
Using the isMounted hack uses a mutable reference in the useEffect hook that will always be synchronously updated when unmounting, and can be a final check before enqueueing the state update.
useEffect(() => {
const source = axios.CancelToken.source();
const token = source.token;
let isMounted = true;
const fetchProducts = async () => {
try {
const response = await ProductService.getProducts(token);
isMounted && setProducts(response.data);
} catch (error) {
console.log(error.message, error.response.status);
}
};
fetchProducts();
return () => {
isMounted = false;
source.cancel();
};
}, []);
I call this a hack as it's really only a way to skirt/stifle the warning. React state updates to unmounted components are ignored anyway.

How do I properly set up my useEffect so I don't receive a missing dependency warning?

I am receiving this warning "React Hook React.useEffect has missing dependencies: 'fetchData' and 'source'. Either include them or remove the dependency array react-hooks/exhaustive-deps". This is my function:
function EmployeesPage(props: any) {
const companyId = props.match.params.id;
const source = axios.CancelToken.source();
const fetchData = async () => {
try {
const response = await axios.get<IEmployees[]>(`${process.env.PUBLIC_URL}/api/company/${companyId}/employees`, {
cancelToken: source.token
});
setEmployees(response.data);
setLoading(true);
} catch (error) {
if (axios.isCancel(error)) {
} else {
throw error;
}
}
}
const deleteEmployee = async (EmployeeId: any) => {
const response = await axios.delete(`${process.env.PUBLIC_URL}/api/company/${companyId}/employees/${employeeId}`);
if (response) await fetchData();
}
React.useEffect(() => {
fetchData()
return () => {
source.cancel();
};
}, [])
I tried to fix this by putting fetchData inside of the useEffect and moving the deleteEmployee out, but this causes my endpoint to be called in an infinite loop. Then I tried the useCallback function and also created an infinite loop.
const fetchData = useCallback(async () => {
try {
const response = await axios.get<IEmployees[]>(`${process.env.PUBLIC_URL}/api/company/${companyId}/employees`, {
cancelToken: source.token
});
setEmployees(response.data);
setLoading(true);
} catch (error) {
if (axios.isCancel(error)) {
} else {
throw error;
}
}
}, [source, CompanyId]);
React.useEffect(() => {
fetchData()
return () => {
source.cancel();
};
}, [fetchData, source])
const deleteEmployee = async (EmployeeId: any) => {
await axios.delete(`${process.env.PUBLIC_URL}/api/company/${companyId}/employees/${employeeId}`);
}
It is my understanding that the only thing that should be going in the dependency array would be something that is going to change. I think my dependency array should be empty because I don't want anything to change. It is going to be the same data being returned each time unless a new employee is added. I'm not sure how to fix this to get the warning message to go away. I have see that there is a way to disable the warning but I am not sure I should do that.
The effect runs in an infinite loop since the source object changes in every render. Move it inside the effect. And move the fetchData function inside the effect as well since it needs access to source.
You should add companyId to the dependencies array to make sure the data is refetched when companyId changes. The setEmployees and setLoading references don't change so it is safe to add them - they won't cause the effects to re-run.
React.useEffect(() => {
const source = axios.CancelToken.source()
const fetchData = async () => {
//...
}
fetchData()
return () => {
source.cancel()
}
}, [companyId, setEmployees, setLoading])
I would recommend reading this to understand if it is safe to omit functions from the dependencies array.
You could declare both fetchData and source inside the useEffect, since it does not use anything besides setState functions. This way, fetchData won't be declared over and over on each re-render.
useEffect(() => {
const source = axios.CancelToken.source();
const fetchData = async () => {
...
};
fetchData();
return () => {
source.cancel();
};
}, [setEmployee, setLoading]);

setState never gets set in useEffect after API call responds with data

I'm trying to update state immediately after data comes in from the API. The data is coming in, I can see it using the console.log right below my API request. All of the data is right but for some reason, setState never sets in my hook. It just returns and empty array even after the console displays data.
const [experienceData, setExperienceData] = useState([]);
const { match = {} } = props;
useEffect(() => {
async function fetchData() {
if (Object.keys(match.params).length > 0) {
const response = await ApiService.getExperiences(match.params.experieneId);
console.log(response)
setExperienceData(response)
}
}
fetchData();
}, []);
I must be doing something wrong but I can't figure out what that is. Hoping someone on here has run into the same issue.
UPDATE: I just changed everything over the a class and duplicated the exact code on another file and ran into the exact same issue. The console updates with the data, but the setState on the line immediately after the data does not setState.
async componentDidMount() {
if (Object.keys(this.props.match.params).length > 0) {
const response = await ApiService.getExperiences(this.props.match.params.experieneId);
console.log(response[0])
this.setState({ experienceData: response[0], occurrenceData: response[0].occurrences });
}
}
You have to useSetState in a proper way, the issue is in the setExperienceData
const [experienceData, setExperienceData] = useState({response:""});
const { match = {} } = props;
useEffect(() => {
async function fetchData() {
if (Object.keys(props.match.params).length > 0) {
const response = await ApiService.getExperiences(match.params.experieneId);
console.log(response)
setExperienceData(experienceData => ({ ...experienceData, response: response }));
}
}
fetchData();
}, []);
return(<div>check {experienceData.response}</div>)
I see you left the dependency array empty. This tells React to run this effect only once: when the component first renders. If you want your useEffect to respect your state hook, put setExperienceData inside the dependency array
const [experienceData, setExperienceData] = useState([]);
const { match = {} } = props;
useEffect(() => {
fetchData();
}, [props.match.params]);
const async fetchData = () => {
if (Object.keys(match.params).length > 0) {
const response = await ApiService.getExperiences(match.params.experieneId);
console.log(response)
setExperienceData([...response])
}
}
Could you please try passing [match.params] as the second argument to your useEffect.

useFetch Can't perform a React state update on an unmounted component warning

I have a custom hook that I'm using to make API requests on my react front-end application but the hook seems to be having a bug.
It makes API requests as intended but whenever I unmount the current container/page in which the request is being made, my hook doesn't know that the page has been unmounted so it doesn't cancel the request and therefore react throws the 'Can't perform a React state update on an unmounted component' warning.
export function useFetch(initialValue, url, options, key) {
const [response, setResponse] = useLocalStorage(key, initialValue);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const isMounted = { state: true };
async function fetchData() {
setLoading(true);
try {
const res = await axios({
url: url,
baseURL: BASE_URL,
cancelToken: source.token,
...options
});
if (res.data.results) {
setResponse(res.data.results);
} else {
setResponse(res.data);
}
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
}
if (isMounted.state) {
fetchData();
}
return () => {
isMounted.state = false;
source.cancel('Operation canceled by the user.');
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
return [response, { error, loading }];
}
By now you are checking for if(isMounter.state) in wrong place. It's currently very next step after you've initialized it.
I believe it should be
const isMounted = { state: true };
async function fetchData() {
setLoading(true);
try {
const res = await axios({
url: url,
baseURL: BASE_URL,
cancelToken: source.token,
...options
});
if(!isMounted.state) return;
.....
}
}
fetchData();
BTW you don't have to use object there: isMounted = true/isMounted = false will work just fine through closure.
Actually your have 2 different approaches mixed: using flag(isMounted) and cancelling request. You may use just one. Cancelling request should work(as far as I see) but it leads your catch block is executed:
} catch (error) {
setError(error);
setLoading(false);
}
See, unmounting cancels request, but your code still tries to set up some state. Probably you better check if request has been failed or canceled with axious.isCancel:
} catch (error) {
if (!axios.isCancel(error)) {
setError(error);
setLoading(false);
}
}
And you may get rid of isMounted in this case.
I use the following hook to get an ifMounted function
const useIfMounted = () => {
const isMounted = useRef(true)
useEffect(
() => () => {
isMounted.current = false
},[]
)
const ifMounted = useCallback(
func => {
if (isMounted.current && func) {
func()
}
},[]
)
return ifMounted
}
Then in your code add const ifMounted = useIfMounted() to useFetch and before your set functions do ifMounted(() => setLoading(true), ifMounted(() => setError(error)), etc....
Here's a blog post I wrote on the subject: https://aceluby.github.io/blog/react-hooks-cant-set-state-on-an-unmounted-component

Resources