In React, failing to stop Axios request when component unmounts - javascript

With data fetching in React, the following is a common 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. in ParentComponent
I've read multiple posts and suggestions on how to handle this, and none are working currently.
For this, we have the function useAxiosApi which fetches data asynchronously, and ParentComponent which is the component that uses the useAxiosApi() and needs the data. ParentComponent is the component being unmounted / being referenced in the warnings.
Parent Component
import useAxiosApi...
function ParentComponent({ info }) {
const dataConfig = { season: info.season, scope: info.scope };
const [data, isLoading1, isError1] = useAxiosApi('this-endpoint', [], dataConfig);
return (
{isLoading && <p>We are loading...</p>}
{!isLoading &&
... use the data to render something...
}
)
}
useAxiosApi
import axios from 'axios';
import { useState } from 'react';
import useDeepCompareEffect from 'use-deep-compare-effect';
const resources = {};
const useAxiosApi = (endpoint, initialValue, config) => {
// Set Data-Fetching State
const [data, setData] = useState(initialValue);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// For Live Search on keystroke, Save Fetches and Skip Fetch if Already Made
if (endpoint === 'liveSearch' && resources[config.searchText]) {
return [resources[config.searchText], false, false];
}
// Otherwise, Continue Forward
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
console.log('isMounted: ', isMounted);
if (isMounted) {
setData(result.data);
}
// If LiveSearch, store the response to "resources"
if (endpoint === 'liveSearch') {
resources[config.searchText] = result.data;
}
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
}
};
// Call Function
fetchData();
// Cancel Request if needed in cleanup function
return () => {
console.log('Unmount or New Search? About to call source.cancel()');
isMounted = false; // is this doing its job?
source.cancel();
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
export default useAxiosApi;
createUrl is simply a function that takes the endpoint and dataConfig and creates the url that axios will fetch from. Note that our cancelTokens seem to be working in conjunction with the Live search, as new searches are cancelling the old search queries, and the saving of data results into resources for the one specific endpoint liveSearch works as well.
However, our problem is that when ParentComponent is unmounted quickly, before the data fetch is complete, we still receive the Cant perform a React state update warning. I've checked the console.logs(), and console.log('isMounted: ', isMounted) is always returning true, even if we unmount the component quickly after it is mounted / before data fetching is complete.
We're at a loss on this, as using the isMounted variable is the way that I've seen this problem handled before. Perhaps there's a problem with the useDeepCompareEffect hook? Or maybe we're missing something else.
Edit: Weve also tried to create the isMounted variable from inside of ParentComponent, and pass that as a parameter into the useAxiosApi function, however this did not work for us either... In general, it would be much better if we can handle this warning via an update to our useAxiosApi function, as opposed to in the ParentComponent.
Edit2: It seems like the cancelToken only works when a duplicate API call is fired off to the same endpoint. This is good for our liveSearch, however it means that all of the other fetches are not cancelled.

Related

React hook returns before it should

I’ve encountered a problem with a React hook from the "react-moralis" SDK and need some help.
I’ve implemented the useWeb3Contract hook from the package in an exported function, which should return the result (data) of the function call after fetching the data successfully. However, the function seems to be returning before the data gets fetched successfully.
Here is the code of the function:
import { useEffect } from "react"
import { useWeb3Contract } from "react-moralis"
import { abi as tokenAbi } from "../../constants/ERC20"
export default function useRetrieveTokenSymbol(address) {
const {
runContractFunction: fetch,
data,
isFetching,
isLoading,
} = useWeb3Contract({
abi: tokenAbi,
contractAddress: address,
functionName: "symbol",
params: {},
})
const retrieve = async () => {
await fetch()
}
useEffect(() => {
retrieve()
}, [])
if (!isFetching && !isLoading && data) return data
}
This function then gets called in a separate react component, where it should only return the data from the useWeb3Contract hook, which then can be stored in a variable.
That looks like that:
export default function TokenSymbol({tokenAddress}) {
const tokenSymbol = useRetrieveTokenSymbol(tokenAddress)
return (
<div>{tokenSymbol}</div>
)
}
tokenSymbol however always is undefined, because the useRetrieveTokenSymbol function returns before finishing fetching the data and then returns an undefined data constant. The conditional if statement gets completely ignored and I simply cannot figure out why.
Any of you guys know a fix to this? Would really appreciate it :)
You should wrap the values you are tracking in state. That way, when they update, the hook will cause a re-render:
const [myData, setMyData] = useState(data);
useEffect(() => {
setMyData(data);
}, [data]);
return myData;

How to prevent state updates from a function, running an async call

so i have a bit of a weird problem i dont know how to solve.
In my code i have a custom hook with a bunch of functionality for a fetching a list
of train journeys. I have some useEffects to that keeps loading in new journeys untill the last journey of the day.
When i change route, while it is still loading in new journeys. I get the "changes to unmounted component" React error.
I understand that i get this error because the component is doing an async fetch that finishes after i've gone to a new page.
The problem i can't figure out is HOW do i prevent it from doing that? the "unmounted" error always occur on one of the 4 lines listed in the code snippet.
Mock of the code:
const [loading, setLoading] = useState(true);
const [journeys, setJourneys] = useState([]);
const [hasLaterDepartures, setHasLaterDepartures] = useState(true);
const getJourneys = async (date, journeys) => {
setLoading(true);
setHasLaterDepartures(true);
const selectedDateJourneys = await fetchJourney(date); // Fetch that returns 0-3 journeys
if (condition1) setHasLaterDepartures(false); // trying to update unmounted component
if (condition2) {
if (condition3) {
setJourneys(something1); // trying to update unmounted component
} else {
setJourneys(something2) // trying to update unmounted component
}
} else {
setJourneys(something3); // trying to update unmounted component
}
};
// useEffects for continous loading of journeys.
useEffect(() => {
if (!hasLaterDepartures) setLoading(false);
}, [hasLaterDepartures]);
useEffect(() => {
if (hasLaterDepartures && journeys.length > 0) {
const latestStart = ... // just a date
if (latestStart.addMinutes(5).isSameDay(latestStart)) {
getJourneys(latestStart.addMinutes(5), journeys);
} else {
setLoading(false);
}
}
}, [journeys]);
I can't use a variable like isMounted = true in the useEffect beacuse it would reach inside the if statement and reach a "setState" by the time i'm on another page.
Moving the entire call into a useEffect doesn't seem to work either. I am at a loss.
Create a variable called mounted with useRef, initialised as true. Then add an effect to set mounted.current to false when the component unmounts.
You can use mounted.current anywhere inside the component to see if it's mounted, and check that before setting any state.
useRef gives you a variable you can mutate but which doesn't cause a rerender.
When you use useEffect hook with action which can be done after component change you should also take care about clean effect when needed. Maybe example help you, also check this page.
useEffect(() => {
let isClosed = false
const fetchData = async () => {
const data = await response.json()
if ( !isClosed ) {
setState( data )
}
};
fetchData()
return () => {
isClosed = true
};
}, []);
In your use case, you probably want to create a Store that doesn't reload everytime you change route (client side).
Example of a store using useContext();
const MyStoreContext = createContext()
export function useMyStore() {
const context = useContext(MyStoreContext)
if (!context && typeof window !== 'undefined') {
throw new Error(`useMyStore must be used within a MyStoreContext`)
}
return context
}
export function MyStoreProvider(props) {
const [ myState, setMyState ] = useState()
//....whatever codes u doing with ur hook.
const exampleCustomFunction = () => {
return myState
}
const getAllRoutes = async (mydestination) => {
return await getAllMyRoutesFromApi(mydestination)
}
// you return all your "getter" and "setter" in value props so you can use them outside the store.
return <MyStoreContext.Provider value={{ myState, setMyState, exampleCustomFunction, getAllRoutes }}>{props.children}</MyStoreContext.Provider>
}
You will wrap the store around your entire App, e.g.
<MyStoreProvider>
<App />
</MyStoreProvider>
In your page where you want to use your hook, you can do
const { myState, setMyState, exampleCustomFunction, getAllRoutes } = useMyStore()
const onClick = async () => getAllRouters(mydestination)
Considering if you have client side routing (not server side), this doesn't get reloaded every time you change your route.

Can you use an async function to set initial state with useState

My component relies on local state (useState), but the initial value should come from an http response.
Can I pass an async function to set the initial state? How can I set the initial state from the response?
This is my code
const fcads = () => {
let good;
Axios.get(`/admin/getallads`).then((res) => {
good = res.data.map((item) => item._id);
});
return good;
};
const [allads, setAllads] = useState(() => fcads());
But when I try console.log(allads) I got result undefined.
If you use a function as an argument for useState it has to be synchronous.
The code your example shows is asynchronous - it uses a promise that sets the value only after the request is completed
You are trying to load data when a component is rendered for the first time - this is a very common use case and there are many libraries that handle it, like these popular choices: https://www.npmjs.com/package/react-async-hook and https://www.npmjs.com/package/#react-hook/async. They would not only set the data to display, but provide you a flag to use and show a loader or display an error if such has happened
This is basically how you would set initial state when you have to set it asynchronously
const [allads, setAllads] = useState([]);
const [loading, setLoading] = useState(false);
React.useEffect(() => {
// Show a loading animation/message while loading
setLoading(true);
// Invoke async request
Axios.get(`/admin/getallads`).then((res) => {
const ads = res.data.map((item) => item._id);
// Set some items after a successful response
setAllAds(ads):
})
.catch(e => alert(`Getting data failed: ${e.message}`))
.finally(() => setLoading(false))
// No variable dependencies means this would run only once after the first render
}, []);
Think of the initial value of useState as something raw that you can set immediately. You know you would be display handling a list (array) of items, then the initial value should be an empty array. useState only accept a function to cover a bit more expensive cases that would otherwise get evaluated on each render pass. Like reading from local/session storage
const [allads, setAllads] = useState(() => {
const asText = localStorage.getItem('myStoredList');
const ads = asText ? JSON.parse(asText) : [];
return ads;
});
You can use the custom hook to include a callback function for useState with use-state-with-callback npm package.
npm install use-state-with-callback
For your case:
import React from "react";
import Axios from "axios";
import useStateWithCallback from "use-state-with-callback";
export default function App() {
const [allads, setAllads] = useStateWithCallback([], (allads) => {
let good;
Axios.get("https://fakestoreapi.com/products").then((res) => {
good = res.data.map((item) => item.id);
console.log(good);
setAllads(good);
});
});
return (
<div className="App">
<h1> {allads} </h1>
</div>
);
}
Demo & Code: https://codesandbox.io/s/distracted-torvalds-s5c8c?file=/src/App.js

Cancel API calls on Unmount

When I am canceling API calls on unmounting and again visiting the same page, the apis are not called again.
const cancelToken = axios.CancelToken;
export const signal = cancelToken.source();
I am using this in my baseconfig.js for Axios, and my component has this in the useEffect
useEffect(() => {
return () => {
props.dispatch({
type: LEAVE_PAGE,
});
console.log("Unmounting");
signal.cancel();
};
},[]);
The API is being canceled as soon as I leave the page or component is unmounted, but on visiting again the API that was canceled is not being called.
I am using the same token for all the apis.
You have export const signal = cancelToken.source(); written above. This implies to me that you are creating one global signal, external to the component, but mutating it (e.g. by cancelling it) within the component.
You should instead instantiate this signal within your component and use it there. That way, you will get a new signal each time the component mounts. You can make sure to not create a new signal on every re-render by using e.g. useMemo. Consider:
const YourComponent = (props) => {
const signal = useMemo(() => axios.CancelToken.source(), []);
useEffect(() => {
return () => {
props.dispatch({
type: LEAVE_PAGE,
});
console.log("Unmounting");
signal.cancel();
};
}, []);
...
};

How to handle/chain synchronous side effects that depend on another with React hooks

I'm trying to rewrite my application from redux to the new context + hooks, but unfortunately I'm having a hard finding a good way to handle series of synchronous side-effects that are depending on the response of the previous.
In my current redux application I make heavy use of synchronous/chained actions and API requests which I typically handle through redux-saga or thunks. So when the response of the first API request is returned, that data is used for the next API request etc.
I've made a custom hook "useFetch" (in this example it does not do much, since it's a simplified version, also I had to make a small adjustment for it to work on codesandbox - see the code below). The problem with that is that due to the "rules of hooks", I cannot use a custom hook inside the useEffect hook. So how to await the response of the first request before doing the next etc, if you have your own hook for fetching data? And even if I ended up giving up on useFetch abstraction and create a vanilla fetch request, how to avoid ending up with a bloated mess of many useEffects hooks? Can this be done a little more elegantly, or is context + hooks still to premature to compete with redux saga/thunk for handling side effects?
The example code below is kept very simple. What it should try to simulate is that:
query person api endpoint to get the person
once we have the person
response, query the job endpoint (using the person id in real world
scenario)
once we have the person and job, based on the response
from the person and job endpoints, query the collegues endpoint to
find the persons collegues at a specific job.
Here's the code. Added a delay to useFetch hook to simulate latency in real world:
import React, { useEffect, useState } from "react";
import { render } from "react-dom";
import "./styles.css";
const useFetch = (url, delay = 0) => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
// const result = await fetch(url, {
// method: "GET",
// headers: { "Content-Type": "application/json" }
// });
//const response = await result.json();
const response = await import(url);
setTimeout(function() {
setData(response);
}, delay);
};
fetchData();
}, [url]);
return data;
};
function App() {
const [person, setPerson] = useState();
const [job, setJob] = useState();
const [collegues, setCollegues] = useState();
// first we fetch the person /api/person based on the jwt most likely
const personData = useFetch("./person.json", 5000);
// now that we have the person data, we use the id to query for the
// persons job /api/person/1/jobs
const jobData = useFetch("./job.json", 3000);
// now we can query for a persons collegues at job x /api/person/1/job/1/collegues
const colleguesData = useFetch("./collegues.json", 1000);
console.log(personData);
console.log(jobData);
console.log(colleguesData);
// useEffect(() => {
// setPerson(useFetch("./person.json", 5000));
// }, []);
// useEffect(() => {
// setJob(useFetch("./job.json", 3000));
// }, [person]);
// useEffect(() => {
// setCollegues(useFetch("./collegues.json",1000));
// }, [job]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);
Running example: https://codesandbox.io/s/2v44lron3n?fontsize=14 (you might need to make a change - space or remove a semicolon - to make it work)
Hopefully something like this (or a better solution) is possible, or I will simply not be able to migrate from the awesome redux-saga/thunks to context + hooks.
Best Answer: https://www.youtube.com/watch?v=y55rLsSNUiM
Hooks wont replace the way you handle async actions, they are just an abstraction to some things you were used to do, like calling componentDidMount, or handling state, etc.
In the example you are giving, you don't really need a custom hook:
function App() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const job = await import("./job.json");
const collegues = await import("./collegues.json");
const person = await import("./person.json");
setData({
job,
collegues,
person
})
};
fetchData()
}, []);
return <div className="App">{JSON.stringify(data)}</div>;
}
That being said, maybe if you provide an example of an actual redux-saga or thunks code you have, that you want to refactor, we can see what the steps are to accomplish that.
Edit:
That being said if you still want to do something like this, you can take a look at this:
https://github.com/dai-shi/react-hooks-async
import React from 'react';
import { useFetch } from 'react-hooks-async/dist/use-async-task-fetch';
const UserInfo = ({ id }) => {
const url = `https://reqres.in/api/users/${id}?delay=1`;
const { pending, error, result, abort } = useFetch(url);
if (pending) return <div>Loading...<button onClick={abort}>Abort</button></div>;
if (error) return <div>Error:{error.name}{' '}{error.message}</div>;
if (!result) return <div>No result</div>;
return <div>First Name:{result.data.first_name}</div>;
};
const App = () => (
<div>
<UserInfo id={'1'} />
<UserInfo id={'2'} />
</div>
);
EDIT
This is an interesting approach https://swr.now.sh/#dependent-fetching
This is a common scenario in real life,where you want to wait for the first fetch is finished and then do the next fetch.
Please check out the new codesandbox:
https://codesandbox.io/s/p92ylrymkj
I used generator when you do fetch request. The data is retrieved in the correct order. After you click fetch data button, go to console and have a look.
Hope this is what you are looking for.
I believe you face this problem because of poor implementation of a fetch hook and the example is too basic for us to understand. You should make it clear if you'll use job, collegues and person in the same component. If so, it's always wiser to separate them.
That being said, let me give you an example of my own fetch hook:
const { loading, error, value } = useResource<Person>(getPerson, personId)
I have such a hook, which has it's own state for loading, error, value etc.
It takes two arguments:
- fetch method
- fetch method argument
Having such a structure, you can chain your resources together.
useResource implementation is just creating a state and in useEffect, checking if property change etc. If it changed, it calls the fetch method.

Categories

Resources