refac!: change accessibility for request hook

BREAKING CHANGES:
now only endpoints with method without side-effects (HEAD, GET)
can be used in `useRequest` hook.

style: change type to interface
This commit is contained in:
Dmitriy Pleshevskiy 2021-06-23 02:05:48 +03:00
parent 1a8ed09d85
commit 39d80815c9
6 changed files with 75 additions and 59 deletions

View file

@ -1,5 +1,5 @@
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import { Method } from './endpoint'; import { Method, methodCanContainBody } from './endpoint';
import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './misc'; import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './misc';
export interface ClientConfig { export interface ClientConfig {
@ -18,19 +18,26 @@ export interface RequestProps<R> extends PrepareRequestProps {
readonly abortSignal: AbortSignal, readonly abortSignal: AbortSignal,
} }
export type ResponseWithError = export interface ResponseWithError
extends
Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'> Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'>
& Readonly<{ error?: Error, canceled?: boolean }> {
readonly error?: Error,
readonly canceled?: boolean,
}
export type ClientResponse<Data extends Record<string, any>> = export interface ClientResponse<Data extends Record<string, any>>
extends
ResponseWithError ResponseWithError
& Readonly<{ data: Data }> {
readonly data: Data
}
export class Client { export class Client {
constructor(private readonly config: ClientConfig) {} constructor(private readonly config: ClientConfig) {}
prepareRequest(props: PrepareRequestProps) { prepareRequest(props: PrepareRequestProps) {
const requestCanContainBody = [Method.POST, Method.PATCH, Method.PUT].includes(props.method); const requestCanContainBody = methodCanContainBody(props.method);
const defaultBaseUrl = (window as Window | undefined)?.location.href; const defaultBaseUrl = (window as Window | undefined)?.location.href;
const sourceUrl = /https?:\/\//.test(props.url) ? const sourceUrl = /https?:\/\//.test(props.url) ?

View file

@ -8,6 +8,14 @@ export enum Method {
DELETE = 'DELETE', DELETE = 'DELETE',
} }
export function methodCanContainBody(method: Method) {
return [Method.POST, Method.PATCH, Method.PUT].includes(method);
}
export function methodWithoutEffects(method: Method) {
return [Method.HEAD, Method.GET].includes(method);
}
export type Endpoint<R, V, P = unknown> = Readonly<{ export type Endpoint<R, V, P = unknown> = Readonly<{
_?: V; // Temporary hack to extract the type of variables. Do not use it in real endpoints. _?: V; // Temporary hack to extract the type of variables. Do not use it in real endpoints.
method: Method; method: Method;

View file

@ -2,19 +2,19 @@ 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 { AnyEndpoint, ExtractEndpointParams, ExtractEndpointResponse, ExtractEndpointVariables } from './endpoint'; import { AnyEndpoint, ExtractEndpointParams, ExtractEndpointResponse, ExtractEndpointVariables, methodWithoutEffects } from './endpoint';
import { PublicRequestState, RequestReducer, requestReducer } from './reducer'; import { 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';
export 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 type LazyRequestConfigFromEndpoint<E extends AnyEndpoint> = LazyRequestConfig< export type LazyRequestConfigFromEndpoint<E extends AnyEndpoint> = LazyRequestConfig<
ExtractEndpointResponse<E>, ExtractEndpointResponse<E>,
@ -22,19 +22,22 @@ export type LazyRequestConfigFromEndpoint<E extends AnyEndpoint> = LazyRequestCo
ExtractEndpointParams<E> ExtractEndpointParams<E>
>; >;
export type LazyRequestHandlerConfig<E extends AnyEndpoint> = Readonly< export interface LazyRequestHandlerConfig<E extends AnyEndpoint>
extends
LazyRequestConfigFromEndpoint<E> LazyRequestConfigFromEndpoint<E>
& { force?: boolean } {
> readonly force?: boolean
}
export type RequestHandler<E extends AnyEndpoint> = export type RequestHandler<E extends AnyEndpoint> =
(config?: LazyRequestHandlerConfig<E>) => Promise<ExtractEndpointResponse<E> | null>; (config?: LazyRequestHandlerConfig<E>) => Promise<ExtractEndpointResponse<E> | null>;
export type PublicRequestStateWithActions<E extends AnyEndpoint> = export interface PublicRequestStateWithActions<E extends AnyEndpoint>
extends
PublicRequestState<ExtractEndpointResponse<E>> PublicRequestState<ExtractEndpointResponse<E>>
& { {
refetch: () => void, readonly refetch: () => void,
cancel: () => void, readonly cancel: () => void,
}; };
export function useLazyRequest<E extends AnyEndpoint>( export function useLazyRequest<E extends AnyEndpoint>(
@ -96,13 +99,16 @@ export function useLazyRequest<E extends AnyEndpoint>(
...handlerConfig?.headers, ...handlerConfig?.headers,
}; };
if ( const shouldReturnCachedValue = (
state.isCalled methodWithoutEffects(endpoint.method)
&& state.isCalled
&& isSameRequest && isSameRequest
&& state?.prevVariables && isEqual(state.prevVariables, variables) && state?.prevVariables && isEqual(state.prevVariables, variables)
&& state?.prevHeaders && isEqual(state.prevHeaders, headers) && state?.prevHeaders && isEqual(state.prevHeaders, headers)
&& handlerConfig?.force === false && !handlerConfig?.force
) { );
if (shouldReturnCachedValue) {
return Promise.resolve(state.data); return Promise.resolve(state.data);
} }
@ -147,10 +153,7 @@ export function useLazyRequest<E extends AnyEndpoint>(
const refetchRequest = React.useCallback( const refetchRequest = React.useCallback(
() => { () => {
if (prevHandlerConfig != null) { if (prevHandlerConfig != null) {
handler({ handler({ ...prevHandlerConfig });
...prevHandlerConfig,
force: true,
});
} }
}, },
[handler, prevHandlerConfig] [handler, prevHandlerConfig]

View file

@ -1,19 +1,19 @@
import { ClientResponse } from './client'; import { ClientResponse } from './client';
export 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 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 type RequestAction<R> = export type RequestAction<R> =
| { | {
@ -72,7 +72,7 @@ export function requestReducer<R>(state: RequestState<R>, action: RequestAction<
case 'cancel': { case 'cancel': {
return { return {
...state, ...state,
isCanceled: false, isCanceled: true,
fetchError: undefined, fetchError: undefined,
}; };
} }

View file

@ -3,10 +3,10 @@ import invariant from 'tiny-invariant';
import { Client } from './client'; import { Client } from './client';
export type RequestContextData = Readonly<{ export interface RequestContextData {
client: Client; readonly client: Client;
defaultHeaders?: Record<string, string>; readonly defaultHeaders?: Record<string, string>;
}> }
const RequestContext = React.createContext<RequestContextData | null>(null); const RequestContext = React.createContext<RequestContextData | null>(null);

View file

@ -1,21 +1,21 @@
import React from 'react'; import React from 'react';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import { AnyEndpoint, Method } from './endpoint'; import { AnyEndpoint, methodWithoutEffects } from './endpoint';
import { LazyRequestConfigFromEndpoint, useLazyRequest } from './lazy_request_hook'; import { LazyRequestConfigFromEndpoint, useLazyRequest } from './lazy_request_hook';
export type RequestConfigFromEndpoint<E extends AnyEndpoint> = Readonly< export interface RequestConfigFromEndpoint<E extends AnyEndpoint>
extends
LazyRequestConfigFromEndpoint<E> LazyRequestConfigFromEndpoint<E>
& { {
skip?: boolean, readonly skip?: boolean,
} }
>
export function useRequest<E extends AnyEndpoint>( export function useRequest<E extends AnyEndpoint>(
endpoint: E, endpoint: E,
config?: RequestConfigFromEndpoint<E>, config?: RequestConfigFromEndpoint<E>,
) { ) {
invariant( invariant(
endpoint.method !== Method.DELETE, methodWithoutEffects(endpoint.method),
`You cannot use useRequest with ${endpoint.method} method` `You cannot use useRequest with ${endpoint.method} method`
); );
@ -25,9 +25,7 @@ export function useRequest<E extends AnyEndpoint>(
React.useEffect( React.useEffect(
() => { () => {
if (!skip) { if (!skip) {
handler({ handler();
force: false,
});
} }
}, },
[skip, handler] [skip, handler]