Skip to content

Commit

Permalink
client: Control request cancellation in EntriesPage
Browse files Browse the repository at this point in the history
This will allow us to reliably cancel the requests when new one starts, avoid memory leaks and also make the code generally cleaner.

https://dev.to/viclafouch/cancel-properly-http-requests-in-react-hooks-and-avoid-memory-leaks-pd7
  • Loading branch information
jtojnar committed May 30, 2021
1 parent 38951f6 commit 28d27f3
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 35 deletions.
2 changes: 1 addition & 1 deletion assets/js/helpers/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const liftToPromiseField = (wrapper) => (f) => (...params) => {
* @return {controller: AbortController, promise: Promise}
*/
export const makeAbortableFetch = (fetch) => (url, opts = {}) => {
let controller = new AbortController();
let controller = opts.abortController || new AbortController();
let promise = fetch(url, {
signal: controller.signal,
...opts
Expand Down
14 changes: 5 additions & 9 deletions assets/js/requests/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,14 @@ function enrichItemsResponse(data) {
/**
* Get all items matching given filter.
*/
export function getItems(filter) {
const { controller, promise } = ajax.get('', {
export function getItems(filter, abortController) {
return ajax.get('', {
body: ajax.makeSearchParams({
...filter,
fromDatetime: filter.fromDatetime ? filter.fromDatetime.toISOString() : filter.fromDatetime
})
});

return {
controller,
promise: promise.then(response => response.json()).then(enrichItemsResponse)
};
}),
abortController,
}).promise.then(response => response.json()).then(enrichItemsResponse);
}

/**
Expand Down
5 changes: 0 additions & 5 deletions assets/js/selfoss-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ var selfoss = {
*/
app: null,

/**
* instance of the currently running XHR that is used to reload the items list
*/
activeAjaxReq: null,

/**
* the html title configured
*/
Expand Down
19 changes: 4 additions & 15 deletions assets/js/selfoss-db-online.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,11 @@ selfoss.dbOnline = {
*
* @return void
*/
reloadList: function(fetchParams) {
if (selfoss.activeAjaxReq !== null) {
selfoss.activeAjaxReq.controller.abort();
}

selfoss.activeAjaxReq = itemsRequests.getItems({
reloadList: function(fetchParams, abortController) {
return itemsRequests.getItems({
...fetchParams,
itemsPerPage: selfoss.config.itemsPerPage
});

let promise = selfoss.activeAjaxReq.promise.then((data) => {
}, abortController).then((data) => {
selfoss.db.setOnline();

if (!selfoss.db.enableOffline.value) {
Expand All @@ -264,19 +258,14 @@ selfoss.dbOnline = {
hasMore: data.hasMore
};
}).catch((error) => {
if (error.name == 'AbortError') {
if (error.name === 'AbortError') {
return;
}

return selfoss.handleAjaxError(error).then(function() {
return selfoss.dbOffline.reloadList(fetchParams);
});
}).finally(() => {
// clean up
selfoss.activeAjaxReq = null;
});

return promise;
}


Expand Down
26 changes: 21 additions & 5 deletions assets/js/templates/EntriesPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import classNames from 'classnames';
import { LocalizationContext } from '../helpers/i18n';
import { HttpError } from '../errors';

function reloadList({ fetchParams, append = false, waitForSync = true, entryId = null, setLoadingState }) {
function reloadList({ fetchParams, abortController, append = false, waitForSync = true, entryId = null, setLoadingState }) {
if (abortController.signal.aborted) {
return Promise.resolve();
}

if (entryId && fetchParams.fromId === undefined) {
fetchParams = {
...fetchParams,
Expand All @@ -28,6 +32,10 @@ function reloadList({ fetchParams, append = false, waitForSync = true, entryId =
setLoadingState(LoadingState.LOADING);

var reload = () => {
if (abortController.signal.aborted) {
return Promise.resolve();
}

let reloader = selfoss.dbOffline.reloadList;

// tag, source and search filtering not supported offline (yet?)
Expand All @@ -49,7 +57,11 @@ function reloadList({ fetchParams, append = false, waitForSync = true, entryId =
}

setLoadingState(LoadingState.LOADING);
return reloader(fetchParams).then(({ entries, hasMore }) => {
return reloader(fetchParams, abortController).then(({ entries, hasMore }) => {
if (abortController.signal.aborted) {
return;
}

setLoadingState(LoadingState.SUCCESS);
selfoss.entriesPage.setHasMore(hasMore);

Expand Down Expand Up @@ -83,6 +95,10 @@ function reloadList({ fetchParams, append = false, waitForSync = true, entryId =
}

}).catch((error) => {
if (abortController.signal.aborted) {
return;
}

if (error instanceof HttpError && error.response.status === 403) {
selfoss.history.push('/#');
// TODO: Use location state once we switch to BrowserRouter
Expand Down Expand Up @@ -165,6 +181,7 @@ export function EntriesPage({ entries, hasMore, loadingState, setLoadingState, s
// Perform the scheduled reload.
React.useEffect(() => {
const append = fromId !== undefined || fromDatetime !== undefined;
const abortController = new AbortController();

reloadList({
// Object with parameters for GET /items and similar API calls
Expand All @@ -179,6 +196,7 @@ export function EntriesPage({ entries, hasMore, loadingState, setLoadingState, s
fromDatetime,
fromId,
},
abortController,
append,
// We do not want to focus the entry on successive loads.
entryId: append ? undefined : initialItemId,
Expand All @@ -194,9 +212,7 @@ export function EntriesPage({ entries, hasMore, loadingState, setLoadingState, s
});

return () => {
if (selfoss.activeAjaxReq !== null) {
selfoss.activeAjaxReq.controller.abort();
}
abortController.abort();
};
}, [params.filter, currentTag, currentSource, initialNavSourcesExpanded, searchText, fromDatetime, fromId, initialItemId, setLoadingState]);

Expand Down

0 comments on commit 28d27f3

Please # to comment.