There is a requirement of cancelling the request calls when navigating away from the page or when the same api call is made multiple calls ( keeping the last one active).
This is how the API is extracted out( just a high level)
AJAX.ts
export async function customAjax(options){
let options = {};
options.headers = { ...options.headers, ...obj.headers };
const response = await fetch(url, options);
await response.json()
}
GET and POST calls are being extracted as
API.ts
const get = (url, extra = {}) => request({ url, type: "GET", ...extra });
const post = (url, payload, extra = {}) => request({ url, data: payload ,type: "POST",
}, ...extra });
In the react component I call these utilities as follows:
function MyComponent(){
useEffect(() => {
makeCall();
}, []);
async function makeCall(){
const { response, error } = await API.post(URL, payload);
// Handling code is not added here
// In the similar fashion GET calls are also made
}
}
I have come across Abortcontroller to cancel request where we could use abort method during unmounting of the component.
Is there a way to do this at a utililty level, may be inside customAjax so that I could avoid writing abort controller code everywhere?
From my understanding... What you describe is no different than a memory leak issue. And the current method for avoiding memory leaks is with the AbortController().
As far as handling this at the "utility level", I don't think this is feasible, and indeed would go against the preferred notion of an api being unaware of what's going on at the React component level; i.e separation of concerns..
So, in order to accomplish your requirement, you'll need to use AbortController(), or a custom implementation using a boolean flag that reflects whether the component is mounted, on a per component basis.
Using the boolean flag, you may be able to accept an argument in your api, passing the flag as a parameter; but again, I think this would be considered an anti-pattern.
I understand you're looking for a minimal implementation; but standard practice is fairly minimal:
useEffect(() => {
let abortController = new AbortController();
// Async code
return () => { abortController.abort(); }
}, []);
Using a boolean flag would be more verbose, and would entail something like this in your case:
useEffect(() => {
let isMounted = true;
customAjax(isMounted);
return () => {
isMounted = false;
}
}, []);
To handle out-of-order ajax responses, you can use a local variable inside the effect. For example,
useEffect(() => {
let ignore = false;
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
if (!ignore) setProduct(json);
}
fetchProduct();
return () => { ignore = true };
}, [productId]);
The ignore variable will ensure that only the latest request's response is updated to state. Reference - https://reactjs.org/docs/hooks-faq.html#performance-optimizations
Regarding memory leak concerns, please see this discussion - https://github.com/reactwg/react-18/discussions/82
Related
So, I know this question has been asked 100's of times, but none of the solutions seems to work in my instance.
I am using useState hook to update state to a value initialValues that gets data returned from a getInitialValues function
const [initialValues, setInitialValues] = useState(getInitialValues());
The getInitialValues function does a logic check and either returns an object or another function retrieveDetails()
const getInitialValues = () => {
let details;
if(!addressDetails) {
details = retrieveDetails();
} else {
details = {
...,
...,
...
};
}
return details;
}
The function, retrieveDetails is an async function that makes an API call, and I await the response and return the object received from the response.
const retrieveDetails = async () => {
const addr = addressDetails[currentAddress];
const { addressLookup } = addr;
const key = process.env.API_KEY;
const query = `?Key=${key}&Id=${addressLookup}`;
const addrDetails = await new AddrService().getAddressDetails(query);
return addrDetails;
}
However, when I log the state initialValues it returns Promise {<pending>}?
Even removing the API call and simple returning an object in it's place renders the same result.
Not sure the best way around this to actually return the object?
Any help would be greatly appreciated.
I don't think there is a way to get initial data to useState asynchronously, at least not yet.
React is not waiting for your data to arrive, the function will keep on running to completion while your async operation is queued (on the event loop side).
The current idiomatic way is to fetch the data in an effect and update the state.
useEffect(() => {
getData(someParam).then(data => setState(data))
}, [someParam])
You can read more about it in the DOCS
This isn't something React's built-in hooks support.
You need to build or import a custom hook.
Want short and simple? Try this:
const addressDetails = useAsyncFunctionResult(retrieveDetails);
and add this in hooks/useAsyncFunctionResults.js
function useAsyncFunctionResult(asyncFunction, dependencies = []) {
const [result, setResult] = React.useState();
React.useEffect(() => {
let mounted = true;
asyncFunction().then((data) => mounted && setResult(data))
return () => { mounted = false; }
}, dependencies]);
return result;
}
Here the retrieveDetails function (the one from the question) will start executing (the ().then bit above) when the hook is first called. The result is kept until unmount. And we get no errors about changing component state after unmounting.
If you later want to add caching, use existing hooks instead if making your own.
There's no official useAsync in React, and likely never will be, because if your Promise did a request and the requesting component did unmount before the promise resolves, then best practice is to cancel which differs case by case.
const retrieveDetails = async () => {
const addr = addressDetails[currentAddress];
const { addressLookup } = addr;
const key = process.env.API_KEY;
const query = `?Key=${key}&Id=${addressLookup}`;
const addrDetails = await Promise.resolve(new AddrService().getAddressDetails(query))
return addrDetails;
}
**try this once changed the function a bit**
I have a react hook useDbReadTable for reading data from a database that accepts initial data of tablename and query. It returns an object that includes an isLoading status in addition to the data from the database.
I want to wrap this hook in a new hook that accepts initial data of an array of { tablename, query }, and returns an object with the data from the database for each table, but with the isLoading statuses consolidated into a single boolean based on logic in my new hook.
The idea is, the caller of the new hook can ask for data from a number of tables, but only has to check one status value.
My thought was to have the new hook look something like,
EDIT: Updated code (I had pasted the wrong version)
export const useDbRead = tableReads => {
let myState = {};
for (let i = 0; i < tableReads.length; ++i) {
const { tablename, query = {} } = tableReads[i];
const [{ isLoading, isDbError, dbError, data }] = useDbReadTable(tablename, query);
myState = { ...myState, [tablename]: { isLoading, isDbError, dbError, data }};
}
const finalState = {
...myState,
isLoading: Object.values(myState).reduce((acc, t) => acc || t.isLoading, false),
};
return [finalState];
};
However, eslint gives me this error on my useDbReadTable call:
React Hook "useDbReadTable" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render. react-hooks/rules-of-hooks
And Rules for Hooks says,
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)
After reading the rule and the explanation, it seems the only issue is making sure the hooks are called in the same order on all re-renders. As long as I ensure the list of tables I pass in to my new hook never changes, shouldn't my new hook work fine (as my initial tests indicate)? Or am I missing something?
More importantly, is there a better idea how to implement this, that doesn't violate the Rules of Hooks?
Edit2: in case its helpful, here's useDbReadTable. Note that it includes more functionality than I mention in my question, since I wanted to keep the question as simple as possible. My question is whether my useDbRead is a good solution, or is there a good way to do it without violating the Rules of Hooks?
export const useDbReadTable = (initialTableName, initialQuery = {}, initialData = []) => {
const dbChangeFlag = useSelector(({appState}) => appState.dbChangeFlag);
const [tableName, setTableName] = useState(initialTableName);
const [query, setQuery] = useState(initialQuery);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isDbError: false,
dbError: {},
data: initialData,
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: dataFetch.FETCH_INIT });
try {
const result = Array.isArray(query) ?
await db[tableName].batchGet(query) // query is an array of Ids
:
await db[tableName].find(query);
if (!didCancel) {
dispatch({ type: dataFetch.FETCH_SUCCESS, payload: result });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: dataFetch.FETCH_FAILURE, payload: error });
}
}
};
fetchData().then(); // .then() gets rid of eslint warning
return () => {
didCancel = true;
};
}, [query, tableName, dbChangeFlag]);
return [state, setQuery, setTableName];
};
You can probably avoid using the useDbReadSingle by making useDbRead itself array aware. Something like:
export const useDbRead = tableReads => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const doIt = async () => {
// you would also need to handle the error case, but you get the idea
const data = await Promise.all(
tableReads.map(tr => {
return mydbfn(tr);
})
);
setLoading(false);
};
doIt();
}, [tableReads]);
return { loading, data };
};
When you need to use it for single table read, just call this with a array that has single element.
const {loading, data: [d]} = useDbRead([mytableread])
I have a component that fetches data asynchronously in componentDidMount()
componentDidMount() {
const self = this;
const url = "/some/path";
const data = {}
const config = {
headers: { "Content-Type": "application/json", "Accept": "application/json" }
};
axios.get(url, data, config)
.then(function(response) {
// Do success stuff
self.setState({ .... });
})
.catch(function(error) {
// Do failure stuff
self.setState({ .... });
})
;
}
My test for the component looks like this -
it("renders the component correctly", async () => {
// Have API return some random data;
let data = { some: { random: ["data to be returned"] } };
axios.get.mockResolvedValue({ data: data });
const rendered = render(<MyComponent />);
// Not sure what I should be awaiting here
await ???
// Test that certain elements render
const toggleContainer = rendered.getByTestId("some-test-id");
expect(toggleContainer).not.toBeNull();
});
Since rendering and loading data is async, my expect() statements go ahead and execute before componentDidMount() and the fake async call finish executing, so the expect() statements always fail.
I guess I could introduce some sort of delay, but that feels wrong and of course increases my runtime of my tests.
This similar question and this gist snippet both show how I can test this with Enzyme. Essentially they rely on async/await to call componentDidMount() manually.
However react-testing-library doesn't seem to allow direct access to the component to call its methods directly (probably by design). So I'm not sure "what" to wait on, or whether that's even the right approach.
Thanks!
It depends on what your component is doing. Imagine your component shows a loading message and then a welcome message. You would wait for the welcome message to appear:
const { getByText, findByText } = render(<MyComponent />)
expect(getByText('Loading...')).toBeInTheDocument()
expect(await findByText('Welcome back!')).toBeInTheDocument()
The best way to think about it is to open the browser to look at your component. When do you know that it is loaded? Try to reproduce that in your test.
You need to wrap render with act to solve warning message causes React state updates should be wrapped into act.
e.g:
it("renders the component correctly", async () => {
// Have API return some random data;
let data = { some: { random: ["data to be returned"] } };
axios.get.mockResolvedValue({ data: data });
const rendered = await act(() => render(<MyComponent />));
// Test that certain elements render
const toggleContainer = rendered.getByTestId("some-test-id");
expect(toggleContainer).not.toBeNull();
});
Also same goes for react-testing-library.
Is it possible to automatically throttle all requests going to a particular list of endpoints using axios? Perhaps using axios interceptor?
Currently I throttle the user action that sends the axios request, but the problem with that is that I have to write this everywhere I have a user action that results in some AJAX request. Like this
const throttledDismissNotification = throttle(dismissNotification, 1000)
const dismiss = (event: any) => {
throttledDismissNotification();
};
render() {
return (
<Button onClick={dismiss}>Dismiss Notification</Button>
)
}
This results in a lot of clutter and I was wondering if this could be automated.
Something like:
if(request.url in listOfEndpointsToThrottle && request.params in cacheOfPreviousRequestsToThisEndpoint) {
StopRequest();
}
Obviously this is pseudocode but you get the idea.
Perhaps you could try to use the Cancellation feature that axios provides.
With it, you can ensure that you don't have any two (or more, depending on your implementation) similar requests in a pending state.
Below, you will find a small simplified example of how to ensure that only the latest request is processed. You can adjust it a bit to make it function like a pool of requests
import axios, { CancelToken } from 'axios';
const pendingRequests = {};
const makeCancellable = (headers, requestId) => {
if (!requestId) {
return headers;
}
if (pendingRequests[requestId]) {
// cancel an existing request
pendingRequests[requestId].cancel();
}
const source = CancelToken.source();
const newHeaders = {
...headers,
cancelToken: source.token
};
pendingRequests[requestId] = source;
return newHeaders;
};
const request = ({
url,
method = 'GET',
headers,
id
}) => {
const requestConfig = {
url,
method,
headers: makeCancellable(headers || {}, id)
};
return axios.request(requestConfig)
.then((res) => {
delete pendingRequests[id];
return ({ data: res.data });
})
.catch((error) => {
delete pendingRequests[id];
if (axios.isCancel(error)) {
console.log(`A request to url ${url} was cancelled`); // cancelled
} else {
return handleReject(error);
}
});
};
export default request;
It's quite easy to throttle an axios request itself. The real headache is how to handle the promises that are returned from nullified requests. What is considered sane behavior when dealing with promises that are returned from a nullified axios request? Should they stay pending forever?
I don't see any perfect solution to this problem. But then I come to a solution that is kind of cheating:
What if we don't throttle the axios call, instead we throttle the actual XMLHttpRequest?
This makes things way easier, because it avoids the promise problem, and it's easier to implement. The idea is to implement a cache for recent requests, and if a new request matches a recent one, you just pull the result from cache and skip the XMLHttpRequest.
Because of the way axios interceptors work, the following snippet can be used to skip a certain XHR call conditionally:
// This should be the *last* request interceptor to add
axios.interceptors.request.use(function (config) {
/* check the cache, if hit, then intentionally throw
* this will cause the XHR call to be skipped
* but the error is still handled by response interceptor
* we can then recover from error to the cached response
**/
if (requestCache.isCached(config)) {
const skipXHRError = new Error('skip')
skipXHRError.isSkipXHR = true
skipXHRError.request = config
throw skipXHRError
} else {
/* if not cached yet
* check if request should be throttled
* then open up the cache to wait for a response
**/
if (requestCache.shouldThrottle(config)) {
requestCache.waitForResponse(config)
}
return config;
}
});
// This should be the *first* response interceptor to add
axios.interceptors.response.use(function (response) {
requestCache.setCachedResponse(response.config, response)
return response;
}, function (error) {
/* recover from error back to normality
* but this time we use an cached response result
**/
if (error.isSkipXHR) {
return requestCache.getCachedResponse(error.request)
}
return Promise.reject(error);
});
I have a similar problem, thru my research it seems to lack a good solution. All I saw were some ad hoc solutions so I open an issue for axios, hoping someone can answer my question https://github.com/axios/axios/issues/2118
I also find this article Throttling Axios requests but I did not try the solution he suggested.
And I have a discussion related to this My implementation of debounce axios request left the promise in pending state forever, is there a better way?
I finish one, #hackape thank you for you answer, the code is as follows:
const pendings = {}
const caches = {}
const cacheUtils = {
getUniqueUrl: function (config) {
// you can set the rule based on your own requirement
return config.url + '&' + config.method
},
isCached: function (config) {
let uniqueUrl = this.getUniqueUrl(config)
return caches[uniqueUrl] !== undefined
},
isPending: function (config) {
let uniqueUrl = this.getUniqueUrl(config)
if (!pendings[uniqueUrl]) {
pendings[uniqueUrl] = [config]
return false
} else {
console.log(`cache url: ${uniqueUrl}`)
pendings[uniqueUrl].push(config)
return true
}
},
setCachedResponse: function (config, response) {
let uniqueUrl = this.getUniqueUrl(config)
caches[uniqueUrl] = response
if (pendings[uniqueUrl]) {
pendings[uniqueUrl].forEach(configItem => {
configItem.isFinished = true
})
}
},
getError: function(config) {
const skipXHRError = new Error('skip')
skipXHRError.isSkipXHR = true
skipXHRError.requestConfig = config
return skipXHRError
},
getCachedResponse: function (config) {
let uniqueUrl = this.getUniqueUrl(config)
return caches[uniqueUrl]
}
}
// This should be the *last* request interceptor to add
axios.interceptors.request.use(function (config) {
// to avoid careless bug, only the request that explicitly declares *canCache* parameter can use cache
if (config.canCache) {
if (cacheUtils.isCached(config)) {
let error = cacheUtils.getError(config)
throw error
}
if (cacheUtils.isPending(config)) {
return new Promise((resolve, reject) => {
let interval = setInterval(() => {
if(config.isFinished) {
clearInterval(interval)
let error = cacheUtils.getError(config)
reject(error)
}
}, 200)
});
} else {
// the head of cacheable requests queue, get the response by http request
return config
}
} else {
return config
}
});
In the example provided in the aor-realtime readme
import realtimeSaga from 'aor-realtime';
const observeRequest = (fetchType, resource, params) => {
// Use your apollo client methods here or sockets or whatever else including the following very naive polling mechanism
return {
subscribe(observer) {
const intervalId = setInterval(() => {
fetchData(fetchType, resource, params)
.then(results => observer.next(results)) // New data received, notify the observer
.catch(error => observer.error(error)); // Ouch, an error occured, notify the observer
}, 5000);
const subscription = {
unsubscribe() {
// Clean up after ourselves
clearInterval(intervalId);
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
},
};
};
const customSaga = realtimeSaga(observeRequest);
fetchData function is mentioned, but it's not accessible from that scope, is it just a symbolic/abstract call ?
If I really wanted to refresh the data based on some realtime trigger how could i dispatch the data fetching command from this scope ?
You're right, it is a symbolic/abstract call. It's up to you to implement observeRequest, by initializing it with your restClient for example and calling the client methods accordingly using the fetchType, resource and params parameters.
We currently only use this saga with the aor-simple-graphql-client client