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:
parent
1a8ed09d85
commit
39d80815c9
6 changed files with 75 additions and 59 deletions
|
@ -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) ?
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,20 +22,23 @@ 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>(
|
||||||
endpoint: E,
|
endpoint: E,
|
||||||
|
@ -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]
|
||||||
|
|
|
@ -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,9 +72,9 @@ 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Reference in a new issue