Skip to content

Commit e1e5ed5

Browse files
committed
Improving behavior of the fetch-abort and its visualization during page transitions.
1 parent 135be35 commit e1e5ed5

File tree

11 files changed

+61
-22
lines changed

11 files changed

+61
-22
lines changed

src/client.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ Object.keys(KNOWN_ACE_WORKERS).forEach(key => {
5555
window.Prism = window.Prism || {};
5656
window.Prism.manual = true;
5757

58+
window.addEventListener(
59+
'unhandledrejection',
60+
ev => {
61+
if (ev?.reason?.name === 'AbortError') {
62+
ev.preventDefault();
63+
ev.stopImmediatePropagation();
64+
}
65+
},
66+
true
67+
);
68+
5869
// load the initial state form the server - if any
5970
let state;
6071
const ini = window.__INITIAL_STATE__;

src/components/helpers/FetchManyResourceRenderer/FetchManyResourceRenderer.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33
import { FormattedMessage } from 'react-intl';
44
import { LoadingIcon, WarningIcon } from '../../icons';
5+
import { resourceStatus } from '../../../redux/helpers/resourceManager';
56

6-
const isLoading = status => status === 'PENDING';
7-
const hasFailed = status => status === 'FAILED';
7+
const isLoading = status => status === resourceStatus.PENDING || status === resourceStatus.ABORTED;
8+
const hasFailed = status => status === resourceStatus.FAILED;
89

910
const defaultLoading = noIcons => (
1011
<span>

src/components/helpers/ResourceRenderer/ResourceRenderer.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
isLoading,
1111
isReadyOrReloading,
1212
hasFailed,
13+
hasAborted,
1314
isPosting,
1415
isDeleting,
1516
isDeleted,
@@ -153,7 +154,12 @@ class ResourceRenderer extends Component {
153154

154155
const resources = this.getResources();
155156
const resourcesLength = (resources && (List.isList(resources) ? resources.size : resources.length)) || 0;
156-
const stillLoading = !resources || resources.find(res => !res) || resources.some(isLoading) || forceLoading;
157+
const stillLoading =
158+
!resources ||
159+
forceLoading ||
160+
resources.find(res => !res) ||
161+
resources.some(isLoading) ||
162+
resources.some(hasAborted);
157163
const isReloading =
158164
stillLoading && !forceLoading && resourcesLength > 0 && resources.every(res => res && isReadyOrReloading(res));
159165

src/containers/App/App.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ import './recodex.css';
3434
library.add(regularIcons, solidIcons, brandIcons);
3535

3636
const reloadIsRequired = (...statuses) =>
37-
statuses.includes(resourceStatus.FAILED) && !statuses.includes(resourceStatus.PENDING);
37+
(statuses.includes(resourceStatus.FAILED) || statuses.includes(resourceStatus.ABORTED)) &&
38+
!statuses.includes(resourceStatus.PENDING);
3839

3940
class App extends Component {
4041
static loadAsync =

src/pages/routes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const r = (basePath, component, linkName = '', auth = undefined) => {
8585

8686
/*
8787
* The abort of pending requests was shifted to unmount callback (since new router took away history.listen).
88-
* Top-level components are unmountend only when the page navigates to a different top-level component.
88+
* Top-level components are unmounted only when the page navigates to a different top-level component.
8989
*/
9090
const rootComponent = unwrap(component);
9191
if (rootComponent && rootComponent.prototype) {

src/redux/helpers/api/tools.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export const createApiCallPromise = (
185185
};
186186

187187
/**
188-
* A specific error means that there is a problem with the Internet connectin or the server is down.
188+
* A specific error means that there is a problem with the Internet connecting or the server is down.
189189
* @param {Object} err The error description
190190
* @param {Function} dispatch
191191
*/
@@ -220,7 +220,7 @@ const processResponse = (call, dispatch) =>
220220
})
221221
.then(({ success = true, error = null, payload = {} }) => {
222222
if (!success) {
223-
if (error && error.message && (!error.code || !error.code.startsWith('4'))) {
223+
if (error && error.message && (!error.code || !String(error.code).startsWith('4'))) {
224224
dispatch && dispatch(addNotification(`Server response: ${error.message}`, false));
225225
}
226226
return Promise.reject(error || new Error('The API call was not successful.'));

src/redux/helpers/resourceManager/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
isLoading,
1212
isReadyOrReloading,
1313
hasFailed,
14+
hasAborted,
1415
getError,
1516
getUniqueErrors,
1617
isDeleting,
@@ -33,6 +34,7 @@ export {
3334
isLoading,
3435
isReadyOrReloading,
3536
hasFailed,
37+
hasAborted,
3638
getError,
3739
getUniqueErrors,
3840
isDeleting,

src/redux/helpers/resourceManager/reducerFactory.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ import { resourceStatus } from './status.js';
44

55
export const initialState = fromJS({ resources: {}, fetchManyStatus: {} });
66

7+
const isAbortError = error => error?.name === 'AbortError';
8+
79
const reducerFactory = (actionTypes, id = 'id') => ({
810
[actionTypes.FETCH_PENDING]: (state, { meta }) =>
911
meta.allowReload && state.getIn(['resources', meta[id], 'state']) === resourceStatus.FULFILLED
1012
? state.setIn(['resources', meta[id], 'state'], resourceStatus.RELOADING)
1113
: state.setIn(['resources', meta[id]], createRecord()),
1214

1315
[actionTypes.FETCH_REJECTED]: (state, { meta, payload: error }) =>
14-
state.setIn(['resources', meta[id]], createRecord({ state: resourceStatus.FAILED, error })),
16+
state.setIn(
17+
['resources', meta[id]],
18+
createRecord({ state: isAbortError(error) ? resourceStatus.ABORTED : resourceStatus.FAILED, error })
19+
),
1520

1621
[actionTypes.FETCH_FULFILLED]: (state, { meta, payload: data }) =>
1722
state.setIn(['resources', meta[id]], createRecord({ state: resourceStatus.FULFILLED, data })),
@@ -34,8 +39,8 @@ const reducerFactory = (actionTypes, id = 'id') => ({
3439
: resourceStatus.PENDING
3540
),
3641

37-
[actionTypes.FETCH_MANY_REJECTED]: (state, { meta: { endpoint } }) =>
38-
state.setIn(['fetchManyStatus', endpoint], resourceStatus.FAILED),
42+
[actionTypes.FETCH_MANY_REJECTED]: (state, { meta: { endpoint }, payload: error }) =>
43+
state.setIn(['fetchManyStatus', endpoint], isAbortError(error) ? resourceStatus.ABORTED : resourceStatus.FAILED),
3944

4045
[actionTypes.FETCH_MANY_FULFILLED]: (state, { meta: { endpoint }, payload }) =>
4146
payload

src/redux/helpers/resourceManager/status.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const resourceStatus = {
1010
RELOADING: 'RELOADING', // similar to pending, but the old record data are still available whilst being re-fetched
1111
UPDATING: 'UPDATING', // similar to reloading, but some modification is in progress
1212
FAILED: 'FAILED',
13+
ABORTED: 'ABORTED',
1314
FULFILLED: 'FULFILLED',
1415
POSTING: 'POSTING',
1516
DELETING: 'DELETING',
@@ -21,7 +22,7 @@ export const isLoadingState = state =>
2122

2223
/**
2324
* @param {Object} item The item
24-
* @return {boolean} True when the item is loading or reloaing.
25+
* @return {boolean} True when the item is loading or reloading.
2526
*/
2627
export const isLoading = item => !item || isLoadingState(item.get('state'));
2728

@@ -61,6 +62,12 @@ export const isDeleted = item => !item || item.get('state') === resourceStatus.D
6162
*/
6263
export const hasFailed = item => Boolean(item) && item.get('state') === resourceStatus.FAILED;
6364

65+
/**
66+
* @param {Object} item The item
67+
* @return {boolean} True when the item could not be loaded, written, updated or deleted.
68+
*/
69+
export const hasAborted = item => Boolean(item) && item.get('state') === resourceStatus.ABORTED;
70+
6471
/**
6572
* Return error object { message, code, ... } of a failed resource.
6673
* @param {Object} item

src/redux/middleware/apiMiddleware.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,21 @@ export const apiCall = (
4141
meta: { endpoint, ...meta },
4242
});
4343

44-
const middleware = ({ dispatch, getState }) => next => action => {
45-
switch (action && action.type) {
46-
case CALL_API:
47-
if (!action.request) {
48-
throw new Error('API middleware requires request data in the action');
49-
}
44+
const middleware =
45+
({ dispatch, getState }) =>
46+
next =>
47+
action => {
48+
// skip anything but API calls
49+
if (action?.type !== CALL_API) {
50+
return next(action);
51+
}
5052

51-
action = apiCall(action.request, dispatch, getState);
52-
break;
53-
}
53+
if (!action.request) {
54+
throw new Error('API middleware requires request data in the action');
55+
}
5456

55-
return next(action);
56-
};
57+
action = apiCall(action.request, dispatch, getState);
58+
return next(action);
59+
};
5760

5861
export default middleware;

0 commit comments

Comments
 (0)