in src/hooks/useResource/index.tsx [142:340]
export default function useResource<T, U>({
url,
wsUrl,
initialData = null,
subscribeToEvents = false,
queryParams = emptyObject,
websocketParams,
socketParamFilter,
updatePredicate = defaultUpdatePredicate,
fetchAllData = false,
onUpdate,
onWSUpdate,
pause = false,
useBatching = false,
postRequest,
uuid,
}: HookConfig<T, U>): Resource<T> {
const [error, setError] = useState<APIError | null>(null);
const initData = initialData;
const [result, setResult] = useState<DataModel<T> | null>(null);
const [data, setData] = useState<T | null>(initData);
const [status, statusDispatch] = useReducer(StatusReducer, { id: 0, status: 'NotAsked' });
const q = new URLSearchParams(queryParams).toString();
const target = apiHttp(`${url}${q ? '?' + q : ''}`);
const [retryCounter, setRetryCounter] = useState(0);
// Call batch update
useInterval(() => {
if (useBatching && onUpdate && updateBatcher[target] && (updateBatcher[target] as Array<U>).length > 0) {
onUpdate(updateBatcher[target] as T);
updateBatcher[target] = [];
}
}, 1000);
const websocketUrl = getWebsocketUrl(url, data, wsUrl);
const onWebsocketUpdate = useCallback(
(event: Event<unknown>) => {
if (pause) return;
// If onUpdate or onWSUpdate is given, dont update stateful data object.
if (onUpdate || onWSUpdate) {
// On batching add item to batch object to be sent to view later
if (useBatching) {
if (!updateBatcher[target]) {
updateBatcher[target] = [];
}
(updateBatcher[target] as Array<U>).push(event.data as U);
} else {
if (onWSUpdate) {
onWSUpdate(event.data as U, event.type);
} else if (onUpdate) {
onUpdate((Array.isArray(initialData) ? [event.data] : event.data) as T);
}
}
} else {
if (event.type === EventType.INSERT) {
setData((d) => (Array.isArray(d) ? [event.data].concat(d) : d) as T);
} else if (event.type === EventType.UPDATE) {
setData(
(d) =>
(Array.isArray(d)
? d.map((item) => (updatePredicate(item, event.data as U) ? event.data : item))
: event.data) as T,
);
}
}
},
[initialData, onUpdate, onWSUpdate, pause, target, updatePredicate, useBatching],
);
const useWebsocketQueryParams = useMemo(
() => (socketParamFilter ? socketParamFilter(queryParams || {}) : websocketParams || queryParams),
[queryParams, socketParamFilter, websocketParams],
);
useWebsocket<U>({
url: websocketUrl,
queryParams: useWebsocketQueryParams,
enabled: subscribeToEvents && !pause && !!websocketUrl,
onUpdate: onWebsocketUpdate,
uuid,
});
function newError(targetUrl: string, err: APIError, id: number) {
setLogItem(`ERROR ${targetUrl} ${JSON.stringify(err)}`);
statusDispatch({ type: 'setstatus', id, status: 'Error' });
setError(err);
logWarning(`HTTP error id: ${err.id}, url: ${targetUrl}`);
}
const fetchData = useCallback(
(targetUrl: string, signal: AbortSignal, cb: (isSuccess: boolean) => void, requestid: number) => {
setLogItem(`GET SENT ${targetUrl}`);
fetch(targetUrl, { signal })
.then((response) => {
if (response.status === 200) {
response
.json()
.then((result: DataModel<T>) => {
setLogItem(`GET ${response.status} ${targetUrl} ${JSON.stringify(result)}`);
// If onUpdate, dont store data in stateful data object
if (onUpdate) {
onUpdate(result.data as T, result);
} else {
setData(result.data);
setResult(result);
}
// Trigger postRequest after every request if given.
postRequest && postRequest(true, targetUrl, result);
// If we want all data and we are have next page available we fetch it.
// Else this fetch is done and we call the callback
if (fetchAllData && result.links.next !== null && result.links.next !== targetUrl) {
fetchData(result.links.next || targetUrl, signal, cb, requestid);
} else {
cb(true);
}
})
.catch(() => {
newError(targetUrl, defaultError, requestid);
});
} else {
if (response.status === 403) {
newError(targetUrl, forbiddenError, requestid);
} else if (response.status === 404) {
newError(targetUrl, notFoundError, requestid);
} else {
response
.json()
.then((result) => {
if (typeof result === 'object' && result.id) {
newError(targetUrl, result, requestid);
} else {
newError(targetUrl, defaultError, requestid);
}
postRequest && postRequest(false, targetUrl);
})
.catch((e) => {
console.error('Error decoding JSON for response: ', response, ': ', e);
newError(targetUrl, defaultError, requestid);
postRequest && postRequest(false, targetUrl);
});
}
}
})
.catch((_e) => {
newError(targetUrl, defaultError, requestid);
postRequest && postRequest(false, targetUrl);
});
},
[fetchAllData, onUpdate, postRequest, setData, setResult],
);
useEffect(() => {
const abortCtrl = new AbortController();
const signal = abortCtrl.signal;
let fulfilled = false;
if (!pause) {
const requestid = Date.now();
statusDispatch({ type: 'set', id: requestid, status: 'Loading' });
if (!onUpdate) {
setData(initialData);
}
fetchData(
target,
signal,
(isSuccess) => {
fulfilled = true;
if (isSuccess) {
statusDispatch({ type: 'setstatus', id: requestid, status: 'Ok' });
setError(null);
} else {
newError(target, defaultError, requestid);
}
},
requestid,
);
}
return () => {
if (!fulfilled) {
abortCtrl.abort();
}
};
}, [target, pause, retryCounter, onUpdate, fetchData, initialData]);
const retry = useCallback(() => {
setRetryCounter((val) => val + 1);
}, []);
const getResult = useCallback(() => result, [result]);
return { url, target, data, error, getResult, status: status.status, retry };
}