refac: move abort controller to lazy request hook

This commit is contained in:
Dmitriy Pleshevskiy 2021-06-22 22:45:02 +03:00
parent 807493a5e3
commit f109d87b41
5 changed files with 47 additions and 53 deletions

View file

@ -3,18 +3,19 @@ import { Method } from './endpoint';
import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './misc'; import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './misc';
export interface ClientConfig { export interface ClientConfig {
baseUrl: string, readonly baseUrl: string,
} }
export interface PrepareRequestProps { export interface PrepareRequestProps {
url: string, readonly url: string,
method: Method, readonly method: Method,
headers: Record<string, string>, readonly headers: Record<string, string>,
variables: Record<string, any> | FormData, readonly variables: Record<string, any> | FormData,
} }
export type RequestProps<R> = PrepareRequestProps & { export interface RequestProps<R> extends PrepareRequestProps {
transformResponseData?: (data: unknown) => R, readonly transformResponseData?: (data: unknown) => R,
readonly abortSignal: AbortSignal,
} }
export type ResponseWithError = export type ResponseWithError =
@ -26,8 +27,6 @@ export type ClientResponse<Data extends Record<string, any>> =
& Readonly<{ data: Data }> & Readonly<{ data: Data }>
export class Client { export class Client {
private controller = new AbortController();
constructor(private readonly config: ClientConfig) {} constructor(private readonly config: ClientConfig) {}
prepareRequest(props: PrepareRequestProps) { prepareRequest(props: PrepareRequestProps) {
@ -82,12 +81,13 @@ export class Client {
request<Data extends Record<string, any>>( request<Data extends Record<string, any>>(
{ {
transformResponseData, transformResponseData,
abortSignal,
...restProps ...restProps
}: RequestProps<Data> }: RequestProps<Data>
): Promise<ClientResponse<Data>> { ): Promise<ClientResponse<Data>> {
const req = this.prepareRequest(restProps); const req = this.prepareRequest(restProps);
return fetch(req, { signal: this.controller.signal }) return fetch(req, { signal: abortSignal })
// TODO: need to check response headers and parse json only if content-type header is application/json // TODO: need to check response headers and parse json only if content-type header is application/json
.then( .then(
(res) => Promise.all([res, res.json()]), (res) => Promise.all([res, res.json()]),
@ -131,9 +131,4 @@ export class Client {
return res; return res;
}); });
} }
cancelRequest() {
this.controller.abort();
this.controller = new AbortController();
}
} }

View file

@ -53,6 +53,8 @@ export function useLazyRequest<E extends AnyEndpoint>(
); );
const [prevHandlerConfig, setPrevHandlerConfig] = React.useState<LazyRequestHandlerConfig<E> | null>(null); const [prevHandlerConfig, setPrevHandlerConfig] = React.useState<LazyRequestHandlerConfig<E> | null>(null);
const abortControllerRef = React.useRef(new AbortController());
const transformResponseData = React.useCallback( const transformResponseData = React.useCallback(
(data: unknown): ExtractEndpointResponse<E> => { (data: unknown): ExtractEndpointResponse<E> => {
return isFunction(endpoint.transformResponseData) ? return isFunction(endpoint.transformResponseData) ?
@ -114,6 +116,7 @@ export function useLazyRequest<E extends AnyEndpoint>(
return client return client
.request<ExtractEndpointResponse<E>>({ .request<ExtractEndpointResponse<E>>({
...endpoint, ...endpoint,
abortSignal: abortControllerRef.current.signal,
url: endpointUrl, url: endpointUrl,
headers, headers,
variables, variables,
@ -141,7 +144,7 @@ export function useLazyRequest<E extends AnyEndpoint>(
[state, config, client, endpoint, defaultHeaders, transformResponseData] [state, config, client, endpoint, defaultHeaders, transformResponseData]
); );
const refetch = React.useCallback( const refetchRequest = React.useCallback(
() => { () => {
if (prevHandlerConfig != null) { if (prevHandlerConfig != null) {
handler({ handler({
@ -153,14 +156,15 @@ export function useLazyRequest<E extends AnyEndpoint>(
[handler, prevHandlerConfig] [handler, prevHandlerConfig]
); );
React.useEffect( const cancelRequest = React.useCallback(() => {
() => {
return () => {
dispatch({ type: 'cancel' }); dispatch({ type: 'cancel' });
client.cancelRequest(); abortControllerRef.current.abort();
}; abortControllerRef.current = new AbortController();
}, }, []);
[client]
React.useEffect(
() => cancelRequest,
[cancelRequest]
); );
return [ return [
@ -171,8 +175,8 @@ export function useLazyRequest<E extends AnyEndpoint>(
isCalled: state.isCalled, isCalled: state.isCalled,
isCanceled: state.isCanceled, isCanceled: state.isCanceled,
fetchError: state.fetchError, fetchError: state.fetchError,
refetch, refetch: refetchRequest,
cancel: client.cancelRequest.bind(client), cancel: cancelRequest,
}, },
]; ];
} }

21
target/client.d.ts vendored
View file

@ -1,16 +1,17 @@
import { Method } from './endpoint'; import { Method } from './endpoint';
export interface ClientConfig { export interface ClientConfig {
baseUrl: string; readonly baseUrl: string;
} }
export interface PrepareRequestProps { export interface PrepareRequestProps {
url: string; readonly url: string;
method: Method; readonly method: Method;
headers: Record<string, string>; readonly headers: Record<string, string>;
variables: Record<string, any> | FormData; readonly variables: Record<string, any> | FormData;
}
export interface RequestProps<R> extends PrepareRequestProps {
readonly transformResponseData?: (data: unknown) => R;
readonly abortSignal: AbortSignal;
} }
export declare type RequestProps<R> = PrepareRequestProps & {
transformResponseData?: (data: unknown) => R;
};
export declare type ResponseWithError = Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'> & Readonly<{ export declare type ResponseWithError = Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'> & Readonly<{
error?: Error; error?: Error;
canceled?: boolean; canceled?: boolean;
@ -20,9 +21,7 @@ export declare type ClientResponse<Data extends Record<string, any>> = ResponseW
}>; }>;
export declare class Client { export declare class Client {
private readonly config; private readonly config;
private controller;
constructor(config: ClientConfig); constructor(config: ClientConfig);
prepareRequest(props: PrepareRequestProps): Request; prepareRequest(props: PrepareRequestProps): Request;
request<Data extends Record<string, any>>({ transformResponseData, ...restProps }: RequestProps<Data>): Promise<ClientResponse<Data>>; request<Data extends Record<string, any>>({ transformResponseData, abortSignal, ...restProps }: RequestProps<Data>): Promise<ClientResponse<Data>>;
cancelRequest(): void;
} }

View file

@ -15,7 +15,6 @@ import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './mis
export class Client { export class Client {
constructor(config) { constructor(config) {
this.config = config; this.config = config;
this.controller = new AbortController();
} }
prepareRequest(props) { prepareRequest(props) {
var _a; var _a;
@ -47,9 +46,9 @@ export class Client {
}); });
} }
request(_a) { request(_a) {
var { transformResponseData } = _a, restProps = __rest(_a, ["transformResponseData"]); var { transformResponseData, abortSignal } = _a, restProps = __rest(_a, ["transformResponseData", "abortSignal"]);
const req = this.prepareRequest(restProps); const req = this.prepareRequest(restProps);
return fetch(req, { signal: this.controller.signal }) return fetch(req, { signal: abortSignal })
// TODO: need to check response headers and parse json only if content-type header is application/json // TODO: need to check response headers and parse json only if content-type header is application/json
.then((res) => Promise.all([res, res.json()]), (err) => { .then((res) => Promise.all([res, res.json()]), (err) => {
const canceled = err.name === 'AbortError'; const canceled = err.name === 'AbortError';
@ -89,8 +88,4 @@ export class Client {
return res; return res;
}); });
} }
cancelRequest() {
this.controller.abort();
this.controller = new AbortController();
}
} }

View file

@ -14,6 +14,7 @@ export function useLazyRequest(endpoint, config) {
isCalled: false, isCalled: false,
}); });
const [prevHandlerConfig, setPrevHandlerConfig] = React.useState(null); const [prevHandlerConfig, setPrevHandlerConfig] = React.useState(null);
const abortControllerRef = React.useRef(new AbortController());
const transformResponseData = React.useCallback((data) => { const transformResponseData = React.useCallback((data) => {
return isFunction(endpoint.transformResponseData) ? return isFunction(endpoint.transformResponseData) ?
endpoint.transformResponseData(data) endpoint.transformResponseData(data)
@ -50,7 +51,7 @@ export function useLazyRequest(endpoint, config) {
dispatch({ type: 'call', headers, variables, params }); dispatch({ type: 'call', headers, variables, params });
setPrevHandlerConfig(handlerConfig !== null && handlerConfig !== void 0 ? handlerConfig : {}); setPrevHandlerConfig(handlerConfig !== null && handlerConfig !== void 0 ? handlerConfig : {});
return client return client
.request(Object.assign(Object.assign({}, endpoint), { url: endpointUrl, headers, .request(Object.assign(Object.assign({}, endpoint), { abortSignal: abortControllerRef.current.signal, url: endpointUrl, headers,
variables, variables,
transformResponseData })) transformResponseData }))
.then((response) => { .then((response) => {
@ -65,17 +66,17 @@ export function useLazyRequest(endpoint, config) {
return null; return null;
}); });
}, [state, config, client, endpoint, defaultHeaders, transformResponseData]); }, [state, config, client, endpoint, defaultHeaders, transformResponseData]);
const refetch = React.useCallback(() => { const refetchRequest = React.useCallback(() => {
if (prevHandlerConfig != null) { if (prevHandlerConfig != null) {
handler(Object.assign(Object.assign({}, prevHandlerConfig), { force: true })); handler(Object.assign(Object.assign({}, prevHandlerConfig), { force: true }));
} }
}, [handler, prevHandlerConfig]); }, [handler, prevHandlerConfig]);
React.useEffect(() => { const cancelRequest = React.useCallback(() => {
return () => {
dispatch({ type: 'cancel' }); dispatch({ type: 'cancel' });
client.cancelRequest(); abortControllerRef.current.abort();
}; abortControllerRef.current = new AbortController();
}, [client]); }, []);
React.useEffect(() => cancelRequest, [cancelRequest]);
return [ return [
handler, handler,
{ {
@ -84,8 +85,8 @@ export function useLazyRequest(endpoint, config) {
isCalled: state.isCalled, isCalled: state.isCalled,
isCanceled: state.isCanceled, isCanceled: state.isCanceled,
fetchError: state.fetchError, fetchError: state.fetchError,
refetch, refetch: refetchRequest,
cancel: client.cancelRequest.bind(client), cancel: cancelRequest,
}, },
]; ];
} }