Merge pull request #7 from pleshevskiy/task-5

feat!(request-hook): add new request hook
This commit is contained in:
Dmitriy Pleshevskiy 2020-11-05 22:28:04 +02:00 committed by GitHub
commit 97e5e1de35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 140 additions and 105 deletions

View File

@ -3,14 +3,7 @@ import { useRequest } from 'react-rest-request';
import { MoviesEndpoint, MoviesResponse } from './endpoint';
export default function App() {
const [movies, { data, loading }] = useRequest<MoviesResponse>(MoviesEndpoint);
React.useEffect(
() => {
movies();
},
[movies]
);
const { data, loading } = useRequest<MoviesResponse>(MoviesEndpoint);
return !data ? (
<div>{ loading ? 'Loading...' : 'Something went wrong' }</div>

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "react-rest-request",
"version": "0.1.0",
"version": "0.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,7 @@
export * from './endpoint';
export * from './client';
export * from './client-hook';
export * from './lazy-request-hook';
export * from './request-hook';
export * from './request-context';
export * from './reducer';

115
src/lazy-request-hook.ts Normal file
View File

@ -0,0 +1,115 @@
import React from 'react';
import invariant from 'tiny-invariant';
import isEqual from 'lodash.isequal';
import { useClient } from './client-hook';
import { Endpoint } from './endpoint';
import { PublicRequestState, RequestAction, requestReducer, RequestState } from './reducer';
import { useRequestContext } from './request-context';
export type LazyRequestConfig<R, V, P = void> = Readonly<{
variables?: V;
params?: P;
headers?: Record<string, string>;
onComplete?: (data: R) => unknown;
}>
export type LazyRequestHandlerConfig<R, V, P> = Readonly<
LazyRequestConfig<R, V, P>
& { force?: boolean }
>
export type RequestHandler<R, V, P> = (config?: LazyRequestHandlerConfig<R, V, P>) => Promise<R | null>;
export function useLazyRequest<R = Record<string, any>, V = Record<string, any>, P = void>(
endpoint: Endpoint<P>,
config?: LazyRequestConfig<R, V, P>,
): [RequestHandler<R, V, P>, PublicRequestState<R>] {
const [client] = useClient();
const { defaultHeaders } = useRequestContext();
const [state, dispatch] = React.useReducer<React.Reducer<RequestState<R>, RequestAction<R>>>(
requestReducer,
{
data: null,
loading: false,
isCalled: false,
}
);
const handler = React.useCallback(
(handlerConfig?: LazyRequestHandlerConfig<R, V, P>) => {
if (state?.loading) {
return Promise.resolve(null);
}
let params: P | undefined;
let endpointUrl: string;
let isSameRequest = true;
if (typeof endpoint.url === 'function') {
params = handlerConfig?.params ?? config?.params;
invariant(params, 'Endpoind required params');
endpointUrl = endpoint.url(params);
isSameRequest = !!state?.prevParams && isEqual(state.prevParams, params);
} else {
endpointUrl = endpoint.url;
}
const variables = {
...config?.variables,
...handlerConfig?.variables,
};
const headers = {
...defaultHeaders,
...endpoint.headers,
...config?.headers,
...handlerConfig?.headers,
};
if (
isSameRequest
&& state?.prevVariables && isEqual(state.prevVariables, variables)
&& state?.prevHeaders && isEqual(state.prevHeaders, headers)
&& !handlerConfig?.force
) {
return Promise.resolve(state.data);
}
const onComplete = config?.onComplete ?? handlerConfig?.onComplete;
dispatch({ type: 'call', headers, variables, params });
return client
.request<R>({
...endpoint,
url: endpointUrl,
headers,
variables,
})
.then(
(response) => {
dispatch({ type: 'success', response });
if (typeof onComplete === 'function') {
onComplete(response.data);
}
return response.data;
},
(response) => {
dispatch({ type: 'failure', response });
throw response;
}
);
},
[state, config, client, endpoint, defaultHeaders]
);
return [
handler,
{
data: state.data,
loading: state.loading,
isCalled: state.isCalled,
},
];
}

View File

@ -9,6 +9,8 @@ export type RequestState<R> = Readonly<{
prevParams?: Record<string, any>
}>
export type PublicRequestState<R> = Pick<RequestState<R>, 'data' | 'loading' | 'isCalled'>;
export type RequestAction<R> =
| { type: 'call', headers: Record<string, string>, variables: Record<string, any>, params?: Record<string, any> }
| { type: 'success', response: ClientResponse<R> }

View File

@ -1,111 +1,35 @@
import React from 'react';
import invariant from 'tiny-invariant';
import isEqual from 'lodash.isequal';
import { useClient } from './client-hook';
import { Endpoint } from './endpoint';
import { RequestAction, requestReducer, RequestState } from './reducer';
import { useRequestContext } from './request-context';
import { Endpoint, Method } from './endpoint';
import { LazyRequestConfig, useLazyRequest } from './lazy-request-hook';
export type RequestConfig<R, V, P = void> = Readonly<{
variables?: V;
params?: P;
headers?: Record<string, string>;
onComplete?: (data: R) => unknown;
}>
export type RequestHandlerConfig<R, V, P> = Readonly<
RequestConfig<R, V, P>
& { force?: boolean }
export type RequestConfig<R, V, P> = Readonly<
LazyRequestConfig<R, V, P>
& {
skip?: boolean,
}
>
export type RequestHandler<R, V, P> = (config?: RequestHandlerConfig<R, V, P>) => Promise<R | null>;
export function useRequest<R = Record<string, any>, V = Record<string, any>, P = void>(
endpoint: Endpoint<P>,
config?: RequestConfig<R, V, P>,
): [RequestHandler<R, V, P>, RequestState<R>] {
const [client] = useClient();
const { defaultHeaders } = useRequestContext();
const [state, dispatch] = React.useReducer<React.Reducer<RequestState<R>, RequestAction<R>>>(
requestReducer,
{
data: null,
loading: false,
isCalled: false,
}
) {
invariant(
endpoint.method == Method.GET,
`You cannot use useRequest with ${endpoint.method} method`
);
const handler = React.useCallback(
(handlerConfig?: RequestHandlerConfig<R, V, P>) => {
if (state?.loading) {
return Promise.resolve(null);
const [handler, state] = useLazyRequest(endpoint, config);
const skip = React.useMemo(() => config?.skip ?? false, [config]);
React.useEffect(
() => {
if (!skip) {
handler();
}
let params: P | undefined;
let endpointUrl: string;
let isSameRequest = true;
if (typeof endpoint.url === 'function') {
params = handlerConfig?.params ?? config?.params;
invariant(params, 'Endpoind required params');
endpointUrl = endpoint.url(params);
isSameRequest = !!state?.prevParams && isEqual(state.prevParams, params);
} else {
endpointUrl = endpoint.url;
}
const variables = {
...config?.variables,
...handlerConfig?.variables,
};
const headers = {
...defaultHeaders,
...endpoint.headers,
...config?.headers,
...handlerConfig?.headers,
};
if (
isSameRequest
&& state?.prevVariables && isEqual(state.prevVariables, variables)
&& state?.prevHeaders && isEqual(state.prevHeaders, headers)
&& !handlerConfig?.force
) {
return Promise.resolve(state.data);
}
const onComplete = config?.onComplete ?? handlerConfig?.onComplete;
dispatch({ type: 'call', headers, variables, params });
return client
.request<R>({
...endpoint,
url: endpointUrl,
headers,
variables,
})
.then(
(response) => {
dispatch({ type: 'success', response });
if (typeof onComplete === 'function') {
onComplete(response.data);
}
return response.data;
},
(response) => {
dispatch({ type: 'failure', response });
throw response;
}
);
},
[state, config, client, endpoint, defaultHeaders]
[skip, handler]
);
return [
handler,
state,
];
return state;
}