Background of the Problem:
I am building a React/Redux app that uses redux-thunk and wretch (a fetch wrapper) to handle asynchronous requests.
I have a few search actions that can vary significantly in their load times, causing undesirable behavior.
I have looked into using AbortController(), but it's either cancelling all my requests outright, or failing to cancel the previous request.
example problem:
Request a search for "JOHN", then request a search for "JOHNSON".
Results for "JOHNSON" return first, and then results for "JOHN" return later and overwrite the "JOHNSON" results.
Goal:
Initiating a request should abort previous pending requests.
example desired behavior:
Request a search for "JOHN", then request a search for "JOHNSON".
Upon initiating the request for "JOHNSON", the pending request for "JOHN" is aborted.
Code:
actions.js
The fetchData action gets called via an onClick or by other functions.
import api from '../../utils/request';
export function fetchData(params) {
return dispatch => {
dispatch(requestData());
return api
.query(params)
.url('api/data')
.get()
.fetchError(err => {
console.log(err);
dispatch(apiFail(err.toString()));
})
.json(response => dispatch(receiveData(response.items, response.totalItems)))
}
}
export function requestData() {
return {
type: REQUEST_DATA,
waiting: true,
}
}
export function receiveData(items, totalItems) {
return {
type: RECEIVE_DATA,
result: items,
totalItems: totalItems,
waiting: false,
}
}
export function apiFail(err) {
return {
type: API_FAIL,
error: err,
waiting: false,
}
}
utils/request.js
This is wretch import. Wretch is a fetch wrapper so it should function similarly to fetch.
import wretch from 'wretch';
/**
* Handles Server Error
*
* #param {object} err HTTP Error
*
* #return {undefined} Returns undefined
*/
function handleServerError(err) {
console.error(err);
}
const api = wretch()
.options({ credentials: 'include', mode: 'cors' })
.url(window.appBaseUrl || process.env.REACT_APP_API_HOST_NAME)
.resolve(_ => _.error(handleServerError))
export default api;
Attempt:
I've tried using wretch's .signal() parameter with an AbortController(), calling .abort() after the request, but that aborts all requests, causing my app to break. Example below:
import wretch from 'wretch';
/**
* Handles Server Error
*
* #param {object} err HTTP Error
*
* #return {undefined} Returns undefined
*/
function handleServerError(err) {
console.error(err);
}
const controller = new AbortController();
const api = wretch()
.signal(controller)
.options({ credentials: 'include', mode: 'cors' })
.url(window.appBaseUrl || process.env.REACT_APP_API_HOST_NAME)
.resolve(_ => _.error(handleServerError))
controller.abort();
export default api;
I've tried moving the logic around to various places, but it seems abort all actions or abort none of them.
Any advice as to how to go about this would be appreciated, this is critical for my team.
Thank you
I feel pretty silly right now, but this is what it took to get it working.
Solution Steps:
Set an AbortController to the initialState of the reducer
reducer.js
export default (state = {
controller: new AbortController(),
}, action) => {
switch (action.type) {
...
Get the AbortController from the state, at the beginning of the fetch action and abort it.
Create a new AbortController and pass it into the requestData action.
Pass the new AbortController into the signal() param of the wretch call.
actions.js
export function fetchData(params) {
return (dispatch, getState) => {
const { controller } = getState().reducer;
controller.abort();
const newController = new AbortController();
dispatch(requestData(newController));
return api
.signal(newController)
.query(params)
.url('api/data')
.get()
.fetchError(err => {
console.log(err);
dispatch(apiFail(err.toString()));
})
.json(response => dispatch(receiveData(response.items, response.totalItems)))
}
}
export function requestData(controller) {
return {
type: REQUEST_DATA,
waiting: true,
controller,
}
}
In the reducer, for the case of the requestData action, set the new AbortController to the state.
reducer.js
case REQUEST_DATA:
return {
...state,
waiting: action.waiting,
controller: action.controller
};
There's some additional functionality with wretch, an .onAbort() param, that allows you to dispatch other actions when the request is aborted. I haven't coded that out yet, but I figured I'd include the info for anyone else struggling with this.
Related
I have created a redux that is going to request an API and if the result is 200, I want to redirect the user to another page using history.
The problem is: I don't know how to trigger this change if the action is a success.
I could redirect the user in my useCase function but I can't use history.push pathName/state argument because it only works in a React component.
So this is what I have done in my React component:
const acceptProposalHandler = () => {
store.dispatch(acceptProposal(id)).then(() => {
setTimeout(() => {
if (isAccepted) { //isAccepted is false by default but is changed to true if the
//request is 200
history.push({
pathname: urls.proposal,
state: {
starterTab: formatMessage({id: 'proposalList.tabs.negotiation'}),
},
});
}
}, 3000);
});
};
Sometimes it works but other times it wont. For some reason, .then is called even if the request fails.
I'm using setTimeOut because if I don't, it will just skip the if statement because the redux hasn't updated the state with isAccepted yet.
This is my useCase function from redux:
export const acceptProposal = (id: string) => async (
dispatch: Dispatch<any>,
getState: () => RootState,
) => {
const {auth} = getState();
const data = {
proposalId: id,
};
dispatch(actions.acceptProposal());
try {
await API.put(`/propostas/change-proposal-status/`, data, {
headers: {
version: 'v1',
'Content-Type': 'application/json',
},
});
dispatch(actions.acceptProposalSuccess());
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
}
};
What I'm doing wrong? I'm using Redux with thunk but I'm not familiar with it.
".then is called even if the request fails." <- this is because acceptProposal is catching the API error and not re-throwing it. If an async function does not throw an error, it will resolve (i.e. call the .then). It can re-throw the error so callers will see an error:
export const acceptProposal = (id: string) => async (
// ... other code hidden
} catch (error) {
dispatch(actions.acceptProposalFailed(error));
// ADD: re-throw the error so the caller can use `.catch` or `try/catch`
throw error;
}
};
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
}
});
I'm new into the React and Redux worlds and after a lot of research, I haven't found a way to handle the problem I have:
I need to perform an api call on app init, but the endpoint is in a configuration file. This configuration in the server so it has to be downloaded and read. This is because I need to distribute the app into many servers and each server has a different configuration.
Therefore the api call has to wait until the configuration has been loaded, they must be chained.
I'm using Redux to manage the state of the app so I have an action which downloads the configuration and an other action which performs the api call.
// Config action
export function fetchConfigRequest() {
return {
type: types.FETCH_CONFIG_REQUEST
}
}
export function fetchConfigSuccess(config) {
return {
type: types.FETCH_CONFIG_SUCCESS,
config
}
}
export function fetchConfig() {
return dispatch => {
dispatch(fetchConfigRequest());
return axios.get('config.json')
.then(response => {
dispatch(fetchConfigSuccess(response.data));
})
;
};
}
// Api client action
export function fetchDataRequest() {
return {
type: types.FETCH_DATA_REQUEST
}
}
export function fetchDataSuccess(data) {
return {
type: types.FETCH_DATA_SUCCESS,
data
}
}
export function fetchDataError(error) {
return {
type: types.FETCH_DATA_ERROR,
error
}
}
export function fetchData(filters = {}) {
return dispatch => {
dispatch(fetchDataRequest());
const apiClient = new apiClient({
url: state.config.apiEndpoint
});
return apiClient.Request()
.then(response => {
dispatch(fetchDataSuccess(data));
})
;
};
}
The only way that I got it working is by waiting until config action promise resolves in App component like this:
// App.component.js
componentDidMount() {
this.props.fetchConfig().then(() => {
this.props.fetchData();
});
}
But I don't think this is the best and the most "Redux style" way to do it, so how should I do it?
I've some ideas in my mind but I don't know what would be the best one:
Keep it as it is now
Create an 'app' action which dispatches the fetch config action, waits until config is loaded, and then dispatches the fetch data action
Do it into a custom middleware
Thanks!
I use HTTP requests to get data for my Vue.js application. I have one file called Api.js with the base axios instance:
export default () => {
return axios.create({
baseURL: apiURL,
headers: {
Authorization: `JWT ${store.state.token}`
}
})
}
than I have a file called service.js, which contains the functions for the different endpoints:
export default {
status() {
return Api().get('status/')
}
}
In the .vue file I call the method like that.
created() {
Service.status()
.then(response => {
// do something with the data
})
.catch(e => {
// show exception
})
}
Some exceptions should be handled in Api.js (for example: 401), some other exceptions should be handled in service.js and others in the .vue file. How can I do that?
Disclaimer: I have created two small axios plugins to achieve this specific pattern easily.
axios-middleware
Simple axios HTTP middleware service to simplify hooking to HTTP requests made through Axios.
It uses axios interceptors as mentioned by acdcjunior but it abstracts the use of axios with a commonly known middleware pattern so your app doesn't need to know and deal with the interceptor syntax.
// import your API's axios instance
import http from './api';
import { Service } from 'axios-middleware';
// Create a new service instance
const service = new Service(http);
// We're good to go!
export default service;
You can then use this middleware service to register different middlewares anywhere in your app. A middleware can be as simple as an object or a reusable, easily testable class.
import i18n from './services/i18n';
import toast from './services/toast';
import service from './services/middleware';
import { ApiErrorMiddleware, OtherMiddleware } from './middlewares';
// Then register your middleware instances.
service.register([
// Middleware class instance
new ApiErrorMiddleware(i18n, toast),
new OtherMiddleware(),
// or a simple object
{
onRequest() {
// handle the request
},
onResponseError(error) {
// handle the response error
}
}
]);
Where the ApiErrorMiddleware would be a simple class with the sole responsibility of showing toast messages on error.
export default class ApiErrorMiddleware {
/**
* #param {VueI18n} i18n instance
* #param {Object} toast message service
*/
constructor(i18n, toast) {
this.toast = toast;
this.i18n = i18n;
}
/**
* #param {Object} error
*/
onResponseError(error = {}) {
const { response } = error;
let key = 'errors.default';
if (response && this.i18n.te(`errors.${response.status}`)) {
key = `errors.${response.status}`;
} else if (error.message === 'Network Error') {
key = 'errors.network-error';
} else {
// TODO log unhandled errors
}
this.toast.error(this.i18n.t(key));
}
}
axios-resource
Simple axios resource class to easily interact with a REST endpoint.
Define a resource class. Here, I added onError and onFetchError as examples for your use-case.
import Resource from 'axios-resource';
export default class UserResource extends Resource {
static URL = 'user/{id}';
// This calls `sync` in the background
fetch() {
return super.fetch.apply(this, arguments)
.catch(err => this.onFetchError(err));
}
onFetchError(err) {
// An error occurred while fetching this resource.
}
onError(err) {
// An error occurred with this resource
}
// called for every actions (fetch, create, patch, delete)
sync() {
return super.sync.apply(this, arguments)
.catch((err) => this.onError(err))
}
}
Then, in api.js, create an instance.
import UserResource from './user';
const user = new UserResource();
// GET https://example.com/api/user/me
user.fetch('me')
.then(({ data }) => {
console.log('User data:', data);
});
The error can be dealt with at every step.
in the onFetchError of this specific resource
in the onError of this resource
in a middleware for the app.
You should add axios interceptors:
Axios Interceptors
You can intercept requests or responses before they are handled by
then or catch.
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
Those can (should) be in your Api.js.
I am trying to make my Vue app have server-side rendering. I am using vue-server-renderer (https://www.npmjs.com/package/vue-server-renderer). Client-side rendering is working fine.
My app use vue-router and axios
Here is my server.js:
server.get('*', (request, response) => {
bundleRenderer.renderToString({ url: request.url }, (error, htmlPromise) => {
if (error) {
// Log the error in the console
console.error(error)
// Tell the client something went wrong
return response
.status(500)
.send(error)
}
response.send(layout.replace('<div id=app></div>', htmlPromise))
})
})
getInfo() is the method to fetch server data.
Here is getInfo():
export default {
methods: {
getInfo(api) {
return axios
.get(api || this.$route.params.path)
.then((data) => {
this.data = data
this.$set(this, 'isLoading', false)
})
},
},
}
My server entry is:
import { app, router, store } from './index'
export default context => {
let componentPromises = router.getMatchedComponents().filter((component) => {
return component.methods && component.methods.getInfo
}).map((component) => {
return component.methods.getInfo()
})
return Promise.all(componentPromises).then(() => {
return app
})
}
However, I soon realize that all the components from router.getMatchedComponents() does not have $route or $set. Therefore, the method getInfo() stops working.
The document from https://router.vuejs.org/en/api/router-instance.html is very short and does not provide much information:
router.getMatchedComponents()
Returns an Array of the components (definition/constructor, not
instances) matched by the current route. This is mostly used during
server-side rendering to perform data prefetching.
How can I fix the problem?
I have previously incurred into a similar problem and managed to successfully prefetch data by doing the following:
app.$router.onReady(() => {
const matchedComponents = app.$router.getMatchedComponents()
if (!matchedComponents.length) { /* ... */}
Promise.all(matchedComponents.map((Component: any) => {
if (Component.options.methods.asyncData) {
return Component.options.methods.asyncData({
store: app.$store,
route: app.$router.currentRoute
});
}
})).then(() => { /* your callback here ... */ });
}
According to vue ssr documentation (https://ssr.vuejs.org/en/data.html) the suggested way is to use a custom asyncData method in your component to perform data fetching rather than calling component methods directly:
export default {
asyncData ({ store, route }) {
// return the Promise from the action
return store.dispatch('fetchItem', route.params.id)
}
},