feat: add clear request store

This commit is contained in:
Dmitriy Pleshevskiy 2021-06-23 02:30:19 +03:00
parent 39d80815c9
commit 163822a49f
13 changed files with 114 additions and 68 deletions

View file

@ -3,7 +3,7 @@ import invariant from 'tiny-invariant';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import { useClient } from './client_hook'; import { useClient } from './client_hook';
import { AnyEndpoint, ExtractEndpointParams, ExtractEndpointResponse, ExtractEndpointVariables, methodWithoutEffects } from './endpoint'; import { AnyEndpoint, ExtractEndpointParams, ExtractEndpointResponse, ExtractEndpointVariables, methodWithoutEffects } from './endpoint';
import { PublicRequestState, RequestReducer, requestReducer } from './reducer'; import { INITIAL_REQUEST_STATE, PublicRequestState, RequestReducer, requestReducer } from './reducer';
import { useRequestContext } from './request_context'; import { useRequestContext } from './request_context';
import { ClientResponse } from './client'; import { ClientResponse } from './client';
import { isFunction } from './misc'; import { isFunction } from './misc';
@ -38,6 +38,7 @@ extends
{ {
readonly refetch: () => void, readonly refetch: () => void,
readonly cancel: () => void, readonly cancel: () => void,
readonly clearStore: () => void,
}; };
export function useLazyRequest<E extends AnyEndpoint>( export function useLazyRequest<E extends AnyEndpoint>(
@ -48,11 +49,7 @@ export function useLazyRequest<E extends AnyEndpoint>(
const { defaultHeaders } = useRequestContext(); const { defaultHeaders } = useRequestContext();
const [state, dispatch] = React.useReducer<RequestReducer<ExtractEndpointResponse<E>>>( const [state, dispatch] = React.useReducer<RequestReducer<ExtractEndpointResponse<E>>>(
requestReducer, requestReducer,
{ INITIAL_REQUEST_STATE,
data: null,
loading: false,
isCalled: false,
}
); );
const [prevHandlerConfig, setPrevHandlerConfig] = React.useState<LazyRequestHandlerConfig<E> | null>(null); const [prevHandlerConfig, setPrevHandlerConfig] = React.useState<LazyRequestHandlerConfig<E> | null>(null);
@ -165,6 +162,10 @@ export function useLazyRequest<E extends AnyEndpoint>(
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
}, []); }, []);
const clearRequestStore = React.useCallback(() => {
dispatch({ type: 'clearStore' });
}, []);
React.useEffect( React.useEffect(
() => cancelRequest, () => cancelRequest,
[cancelRequest] [cancelRequest]
@ -180,6 +181,7 @@ export function useLazyRequest<E extends AnyEndpoint>(
fetchError: state.fetchError, fetchError: state.fetchError,
refetch: refetchRequest, refetch: refetchRequest,
cancel: cancelRequest, cancel: cancelRequest,
clearStore: clearRequestStore
}, },
]; ];
} }

View file

@ -33,9 +33,24 @@ export type RequestAction<R> =
| { | {
type: 'cancel' type: 'cancel'
} }
| {
type: 'clearStore'
}
export type RequestReducer<R> = React.Reducer<RequestState<R>, RequestAction<R>> export type RequestReducer<R> = React.Reducer<RequestState<R>, RequestAction<R>>
export const INITIAL_REQUEST_STATE: RequestState<any> = {
data: null,
response: undefined,
fetchError: undefined,
isCanceled: false,
loading: false,
isCalled: false,
prevHeaders: undefined,
prevVariables: undefined,
prevParams: undefined,
};
export function requestReducer<R>(state: RequestState<R>, action: RequestAction<R>): RequestState<R> { export function requestReducer<R>(state: RequestState<R>, action: RequestAction<R>): RequestState<R> {
switch (action.type) { switch (action.type) {
case 'call': { case 'call': {
@ -76,5 +91,9 @@ export function requestReducer<R>(state: RequestState<R>, action: RequestAction<
fetchError: undefined, fetchError: undefined,
}; };
} }
case 'clearStore': {
return INITIAL_REQUEST_STATE;
}
} }
} }

14
target/client.d.ts vendored
View file

@ -12,13 +12,13 @@ export interface RequestProps<R> extends PrepareRequestProps {
readonly transformResponseData?: (data: unknown) => R; readonly transformResponseData?: (data: unknown) => R;
readonly abortSignal: AbortSignal; readonly abortSignal: AbortSignal;
} }
export declare type ResponseWithError = Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'> & Readonly<{ export interface ResponseWithError extends Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'> {
error?: Error; readonly error?: Error;
canceled?: boolean; readonly canceled?: boolean;
}>; }
export declare type ClientResponse<Data extends Record<string, any>> = ResponseWithError & Readonly<{ export interface ClientResponse<Data extends Record<string, any>> extends ResponseWithError {
data: Data; readonly data: Data;
}>; }
export declare class Client { export declare class Client {
private readonly config; private readonly config;
constructor(config: ClientConfig); constructor(config: ClientConfig);

View file

@ -10,7 +10,7 @@ var __rest = (this && this.__rest) || function (s, e) {
return t; return t;
}; };
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import { Method } from './endpoint'; import { methodCanContainBody } from './endpoint';
import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './misc'; import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './misc';
export class Client { export class Client {
constructor(config) { constructor(config) {
@ -18,7 +18,7 @@ export class Client {
} }
prepareRequest(props) { prepareRequest(props) {
var _a; var _a;
const requestCanContainBody = [Method.POST, Method.PATCH, Method.PUT].includes(props.method); const requestCanContainBody = methodCanContainBody(props.method);
const defaultBaseUrl = (_a = window) === null || _a === void 0 ? void 0 : _a.location.href; const defaultBaseUrl = (_a = window) === null || _a === void 0 ? void 0 : _a.location.href;
const sourceUrl = /https?:\/\//.test(props.url) ? const sourceUrl = /https?:\/\//.test(props.url) ?
props.url props.url

View file

@ -6,6 +6,8 @@ export declare enum Method {
PATCH = "PATCH", PATCH = "PATCH",
DELETE = "DELETE" DELETE = "DELETE"
} }
export declare function methodCanContainBody(method: Method): boolean;
export declare function methodWithoutEffects(method: Method): boolean;
export declare type Endpoint<R, V, P = unknown> = Readonly<{ export declare type Endpoint<R, V, P = unknown> = Readonly<{
_?: V; _?: V;
method: Method; method: Method;

View file

@ -7,3 +7,9 @@ export var Method;
Method["PATCH"] = "PATCH"; Method["PATCH"] = "PATCH";
Method["DELETE"] = "DELETE"; Method["DELETE"] = "DELETE";
})(Method || (Method = {})); })(Method || (Method = {}));
export function methodCanContainBody(method) {
return [Method.POST, Method.PATCH, Method.PUT].includes(method);
}
export function methodWithoutEffects(method) {
return [Method.HEAD, Method.GET].includes(method);
}

View file

@ -1,20 +1,21 @@
import { AnyEndpoint, ExtractEndpointParams, ExtractEndpointResponse, ExtractEndpointVariables } from './endpoint'; import { AnyEndpoint, ExtractEndpointParams, ExtractEndpointResponse, ExtractEndpointVariables } from './endpoint';
import { PublicRequestState } from './reducer'; import { PublicRequestState } from './reducer';
import { ClientResponse } from './client'; import { ClientResponse } from './client';
export declare type LazyRequestConfig<R, V, P> = Readonly<{ export interface LazyRequestConfig<R, V, P> {
variables?: V; readonly variables?: V;
params?: P; readonly params?: P;
headers?: Record<string, string>; readonly headers?: Record<string, string>;
onComplete?: (data: R) => unknown; readonly onComplete?: (data: R) => unknown;
onFailure?: (res: ClientResponse<R>) => unknown; readonly onFailure?: (res: ClientResponse<R>) => unknown;
}>; }
export declare type LazyRequestConfigFromEndpoint<E extends AnyEndpoint> = LazyRequestConfig<ExtractEndpointResponse<E>, ExtractEndpointVariables<E>, ExtractEndpointParams<E>>; export declare type LazyRequestConfigFromEndpoint<E extends AnyEndpoint> = LazyRequestConfig<ExtractEndpointResponse<E>, ExtractEndpointVariables<E>, ExtractEndpointParams<E>>;
export declare type LazyRequestHandlerConfig<E extends AnyEndpoint> = Readonly<LazyRequestConfigFromEndpoint<E> & { export interface LazyRequestHandlerConfig<E extends AnyEndpoint> extends LazyRequestConfigFromEndpoint<E> {
force?: boolean; readonly force?: boolean;
}>; }
export declare type RequestHandler<E extends AnyEndpoint> = (config?: LazyRequestHandlerConfig<E>) => Promise<ExtractEndpointResponse<E> | null>; export declare type RequestHandler<E extends AnyEndpoint> = (config?: LazyRequestHandlerConfig<E>) => Promise<ExtractEndpointResponse<E> | null>;
export declare type PublicRequestStateWithActions<E extends AnyEndpoint> = PublicRequestState<ExtractEndpointResponse<E>> & { export interface PublicRequestStateWithActions<E extends AnyEndpoint> extends PublicRequestState<ExtractEndpointResponse<E>> {
refetch: () => void; readonly refetch: () => void;
cancel: () => void; readonly cancel: () => void;
}; readonly clearStore: () => void;
}
export declare function useLazyRequest<E extends AnyEndpoint>(endpoint: E, config?: LazyRequestConfigFromEndpoint<E>): [RequestHandler<E>, PublicRequestStateWithActions<E>]; export declare function useLazyRequest<E extends AnyEndpoint>(endpoint: E, config?: LazyRequestConfigFromEndpoint<E>): [RequestHandler<E>, PublicRequestStateWithActions<E>];

View file

@ -2,17 +2,15 @@ import React from 'react';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import { useClient } from './client_hook'; import { useClient } from './client_hook';
import { requestReducer } from './reducer'; import { methodWithoutEffects } from './endpoint';
import { INITIAL_REQUEST_STATE, requestReducer } from './reducer';
import { useRequestContext } from './request_context'; import { useRequestContext } from './request_context';
import { isFunction } from './misc'; import { isFunction } from './misc';
;
export function useLazyRequest(endpoint, config) { export function useLazyRequest(endpoint, config) {
const [client] = useClient(); const [client] = useClient();
const { defaultHeaders } = useRequestContext(); const { defaultHeaders } = useRequestContext();
const [state, dispatch] = React.useReducer(requestReducer, { const [state, dispatch] = React.useReducer(requestReducer, INITIAL_REQUEST_STATE);
data: null,
loading: false,
isCalled: false,
});
const [prevHandlerConfig, setPrevHandlerConfig] = React.useState(null); const [prevHandlerConfig, setPrevHandlerConfig] = React.useState(null);
const abortControllerRef = React.useRef(new AbortController()); const abortControllerRef = React.useRef(new AbortController());
const transformResponseData = React.useCallback((data) => { const transformResponseData = React.useCallback((data) => {
@ -39,11 +37,13 @@ export function useLazyRequest(endpoint, config) {
} }
const variables = Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.variables), handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.variables); const variables = Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.variables), handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.variables);
const headers = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultHeaders), endpoint.headers), config === null || config === void 0 ? void 0 : config.headers), handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.headers); const headers = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultHeaders), endpoint.headers), config === null || config === void 0 ? void 0 : config.headers), handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.headers);
if (state.isCalled const shouldReturnCachedValue = (methodWithoutEffects(endpoint.method)
&& state.isCalled
&& isSameRequest && isSameRequest
&& (state === null || state === void 0 ? void 0 : state.prevVariables) && isEqual(state.prevVariables, variables) && (state === null || state === void 0 ? void 0 : state.prevVariables) && isEqual(state.prevVariables, variables)
&& (state === null || state === void 0 ? void 0 : state.prevHeaders) && isEqual(state.prevHeaders, headers) && (state === null || state === void 0 ? void 0 : state.prevHeaders) && isEqual(state.prevHeaders, headers)
&& (handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.force) === false) { && !(handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.force));
if (shouldReturnCachedValue) {
return Promise.resolve(state.data); return Promise.resolve(state.data);
} }
const onCompletes = [config === null || config === void 0 ? void 0 : config.onComplete, handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.onComplete].filter(isFunction); const onCompletes = [config === null || config === void 0 ? void 0 : config.onComplete, handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.onComplete].filter(isFunction);
@ -68,7 +68,7 @@ export function useLazyRequest(endpoint, config) {
}, [state, config, client, endpoint, defaultHeaders, transformResponseData]); }, [state, config, client, endpoint, defaultHeaders, transformResponseData]);
const refetchRequest = React.useCallback(() => { const refetchRequest = React.useCallback(() => {
if (prevHandlerConfig != null) { if (prevHandlerConfig != null) {
handler(Object.assign(Object.assign({}, prevHandlerConfig), { force: true })); handler(Object.assign({}, prevHandlerConfig));
} }
}, [handler, prevHandlerConfig]); }, [handler, prevHandlerConfig]);
const cancelRequest = React.useCallback(() => { const cancelRequest = React.useCallback(() => {
@ -76,6 +76,9 @@ export function useLazyRequest(endpoint, config) {
abortControllerRef.current.abort(); abortControllerRef.current.abort();
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
}, []); }, []);
const clearRequestStore = React.useCallback(() => {
dispatch({ type: 'clearStore' });
}, []);
React.useEffect(() => cancelRequest, [cancelRequest]); React.useEffect(() => cancelRequest, [cancelRequest]);
return [ return [
handler, handler,
@ -87,6 +90,7 @@ export function useLazyRequest(endpoint, config) {
fetchError: state.fetchError, fetchError: state.fetchError,
refetch: refetchRequest, refetch: refetchRequest,
cancel: cancelRequest, cancel: cancelRequest,
clearStore: clearRequestStore
}, },
]; ];
} }

29
target/reducer.d.ts vendored
View file

@ -1,18 +1,18 @@
/// <reference types="react" /> /// <reference types="react" />
import { ClientResponse } from './client'; import { ClientResponse } from './client';
export declare type PublicRequestState<R> = Readonly<{ export interface PublicRequestState<R> {
data: R | null; readonly data: R | null;
loading: boolean; readonly loading: boolean;
isCalled: boolean; readonly isCalled: boolean;
isCanceled?: boolean; readonly isCanceled?: boolean;
response?: ClientResponse<R>; readonly response?: ClientResponse<R>;
fetchError?: Error; readonly fetchError?: Error;
}>; }
export declare type RequestState<R> = PublicRequestState<R> & Readonly<{ export interface RequestState<R> extends PublicRequestState<R> {
prevHeaders?: Record<string, string>; readonly prevHeaders?: Record<string, string>;
prevVariables?: Record<string, any>; readonly prevVariables?: Record<string, any>;
prevParams?: Record<string, any>; readonly prevParams?: Record<string, any>;
}>; }
export declare type RequestAction<R> = { export declare type RequestAction<R> = {
type: 'call'; type: 'call';
headers: Record<string, string>; headers: Record<string, string>;
@ -26,6 +26,9 @@ export declare type RequestAction<R> = {
response: ClientResponse<R>; response: ClientResponse<R>;
} | { } | {
type: 'cancel'; type: 'cancel';
} | {
type: 'clearStore';
}; };
export declare type RequestReducer<R> = React.Reducer<RequestState<R>, RequestAction<R>>; export declare type RequestReducer<R> = React.Reducer<RequestState<R>, RequestAction<R>>;
export declare const INITIAL_REQUEST_STATE: RequestState<any>;
export declare function requestReducer<R>(state: RequestState<R>, action: RequestAction<R>): RequestState<R>; export declare function requestReducer<R>(state: RequestState<R>, action: RequestAction<R>): RequestState<R>;

View file

@ -1,3 +1,14 @@
export const INITIAL_REQUEST_STATE = {
data: null,
response: undefined,
fetchError: undefined,
isCanceled: false,
loading: false,
isCalled: false,
prevHeaders: undefined,
prevVariables: undefined,
prevParams: undefined,
};
export function requestReducer(state, action) { export function requestReducer(state, action) {
switch (action.type) { switch (action.type) {
case 'call': { case 'call': {
@ -10,7 +21,10 @@ export function requestReducer(state, action) {
return Object.assign(Object.assign({}, state), { loading: false, response: action.response, data: null, fetchError: action.response.error, isCanceled: action.response.canceled }); return Object.assign(Object.assign({}, state), { loading: false, response: action.response, data: null, fetchError: action.response.error, isCanceled: action.response.canceled });
} }
case 'cancel': { case 'cancel': {
return Object.assign(Object.assign({}, state), { isCanceled: false, fetchError: undefined }); return Object.assign(Object.assign({}, state), { isCanceled: true, fetchError: undefined });
}
case 'clearStore': {
return INITIAL_REQUEST_STATE;
} }
} }
} }

View file

@ -1,12 +1,9 @@
import React from 'react'; import React from 'react';
import { Client } from './client'; import { Client } from './client';
export declare type RequestContextData = Readonly<{ export interface RequestContextData {
client: Client; readonly client: Client;
defaultHeaders?: Record<string, string>; readonly defaultHeaders?: Record<string, string>;
}>; }
export declare type RequestProviderProps = Readonly<React.PropsWithChildren<RequestContextData>>; export declare type RequestProviderProps = Readonly<React.PropsWithChildren<RequestContextData>>;
export declare function RequestProvider({ client, defaultHeaders, children }: RequestProviderProps): JSX.Element; export declare function RequestProvider({ client, defaultHeaders, children }: RequestProviderProps): JSX.Element;
export declare function useRequestContext(): Readonly<{ export declare function useRequestContext(): RequestContextData;
client: Client;
defaultHeaders?: Record<string, string> | undefined;
}>;

View file

@ -1,6 +1,6 @@
import { AnyEndpoint } from './endpoint'; import { AnyEndpoint } from './endpoint';
import { LazyRequestConfigFromEndpoint } from './lazy_request_hook'; import { LazyRequestConfigFromEndpoint } from './lazy_request_hook';
export declare type RequestConfigFromEndpoint<E extends AnyEndpoint> = Readonly<LazyRequestConfigFromEndpoint<E> & { export interface RequestConfigFromEndpoint<E extends AnyEndpoint> extends LazyRequestConfigFromEndpoint<E> {
skip?: boolean; readonly skip?: boolean;
}>; }
export declare function useRequest<E extends AnyEndpoint>(endpoint: E, config?: RequestConfigFromEndpoint<E>): import("./lazy_request_hook").PublicRequestStateWithActions<E>; export declare function useRequest<E extends AnyEndpoint>(endpoint: E, config?: RequestConfigFromEndpoint<E>): import("./lazy_request_hook").PublicRequestStateWithActions<E>;

View file

@ -1,16 +1,14 @@
import React from 'react'; import React from 'react';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import { Method } from './endpoint'; import { methodWithoutEffects } from './endpoint';
import { useLazyRequest } from './lazy_request_hook'; import { useLazyRequest } from './lazy_request_hook';
export function useRequest(endpoint, config) { export function useRequest(endpoint, config) {
invariant(endpoint.method !== Method.DELETE, `You cannot use useRequest with ${endpoint.method} method`); invariant(methodWithoutEffects(endpoint.method), `You cannot use useRequest with ${endpoint.method} method`);
const [handler, state] = useLazyRequest(endpoint, config); const [handler, state] = useLazyRequest(endpoint, config);
const skip = React.useMemo(() => { var _a; return (_a = config === null || config === void 0 ? void 0 : config.skip) !== null && _a !== void 0 ? _a : false; }, [config]); const skip = React.useMemo(() => { var _a; return (_a = config === null || config === void 0 ? void 0 : config.skip) !== null && _a !== void 0 ? _a : false; }, [config]);
React.useEffect(() => { React.useEffect(() => {
if (!skip) { if (!skip) {
handler({ handler();
force: false,
});
} }
}, [skip, handler]); }, [skip, handler]);
return state; return state;