Cancel API calls on Unmount - javascript

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();
};
}, []);
...
};

Related

How to effectively initialise client register callback in react functional component or hook

I am trining use factory class in react functional component. I need initialise base on components props and do it in first render or when props change.
AS thats why I created instance and store it ref in useEffect with array that dependent on props to get always new instance when prop changed.
But when I do it a need some special state to force component update.
I am still thinking that there is better and cleaner way how to do it. What is strange is the dummy state to force component to update.
So my code that looks is working looks like this:
export const useClientLoader = (props: IClientLoaderProps) => {
const { backend, workspace, filter } = props;
const [,setInvalidate] = useState(0);
const [initStatus,setInitStatus] = useState<status>("pending");
const loaderRef = useRef<IClientLoader>();
const invalidate = ()=>{
// force component update via changing state
setInvalidate(i=>i+1);
}
useEffect(() => {
// init client instance
loaderRef.current = newClientHandler(backend, workspace, filter);
// update ref not update state and re-render component
// so i do force to update by dummy state update
invalidate();
}, [backend, workspace, filter]); // I need new instance when props changed
const onInitSuccess = () => {
setInitStatus("success");
};
// I need current pointer to client instance
const loader = loaderRef.current;
useEffect(() => {
if (loader) {
// subscribe callback
const onInitSuccessUnsubscribe = loader.onInitSuccess(onInitSuccess);
// init client and do magic on backend and wait for result via callback
loader.init();
return () => {
// Unsubscribe callback
onInitSuccessUnsubscribe();
};
}
}, [loader]); // I will do change just when I have new client instance
return {
initStatus:initStatus,
getUser: loader?.getUser
};
};
I guess you can achieve it with just one useEffect, no need to split on two and then to be forced to use workaround in order to trigger rerender. Like this:
const loaderRef = useRef<IClientLoader>();
const onInitSuccess = useCallback(() => {
setInitStatus("success");
}, []);
useEffect(() => {
loaderRef.current = newClientHandler(backend, workspace, filter);
const onInitSuccessUnsubscribe = loaderRef.current.onInitSuccess(onInitSuccess);
loaderRef.current.init();
return () => {
// Unsubscribe callback
onInitSuccessUnsubscribe();
};
}, [backend, workspace, filter, onInitSuccess]);

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

In React, failing to stop Axios request when component unmounts

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.

Creating a React higher order component, to serve as a "loader"(animation) wrapper for child components

I have a lot of components, that require some ajax function being sent, in the componentDidMount method. I would like to create a HOC, whose sole purpose is to "apply" some animation to the component, and stop this animation once a certain promise is resolved.
Of course, i could just copy paste this code for each component, but i would like to create some abstraction that deals with it.
The problem is, that i don't know how to pass the function properly, from the child to the parent. For instance, let's assume the intended child component, has this componentDidMount:
componentDidMount() {
ajax('/costumers')
.then(({ data }) => {
this.setState(() => ({ costumers: data.content }))
})
}
Technically, i need to either pass this function as an argument to the HOC, or perhaps somehow "hijack" the child's componentDidMount(if something like that is possible...). The HOC would then apply an animation once it's loaded, then send the ajax, and only when it's solved, the animation is eliminated, and the child component gets rendered.
How can this be achieved?
Any idea will be appreciated
Here is how you can write a HOC for such a case, refer to React docs for more info on the subject.
const withLoader = (loader, Component) =>
class WithLoader extends React.Component {
state = { ready: false, data: null };
async componentDidMount() {
const data = await loader();
this.setState({ ready: true, data });
}
render() {
if (!this.state.ready) return <div>LOADING</div>; // or <ComponentWithAnimation />
return <Component data={this.state.data} />;
}
};
const Test = props => <div>DATA: {props.data}</div>;
const fakeLoader = () =>
new Promise(res => setTimeout(() => res("My data"), 1000));
const TestWithLoader = withLoader(fakeLoader, Test);

Categories

Resources