Merge pull request #7 from pleshevskiy/task-5
feat!(request-hook): add new request hook
This commit is contained in:
commit
97e5e1de35
6 changed files with 140 additions and 105 deletions
|
@ -3,14 +3,7 @@ import { useRequest } from 'react-rest-request';
|
||||||
import { MoviesEndpoint, MoviesResponse } from './endpoint';
|
import { MoviesEndpoint, MoviesResponse } from './endpoint';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [movies, { data, loading }] = useRequest<MoviesResponse>(MoviesEndpoint);
|
const { data, loading } = useRequest<MoviesResponse>(MoviesEndpoint);
|
||||||
|
|
||||||
React.useEffect(
|
|
||||||
() => {
|
|
||||||
movies();
|
|
||||||
},
|
|
||||||
[movies]
|
|
||||||
);
|
|
||||||
|
|
||||||
return !data ? (
|
return !data ? (
|
||||||
<div>{ loading ? 'Loading...' : 'Something went wrong' }</div>
|
<div>{ loading ? 'Loading...' : 'Something went wrong' }</div>
|
||||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "react-rest-request",
|
"name": "react-rest-request",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export * from './endpoint';
|
export * from './endpoint';
|
||||||
export * from './client';
|
export * from './client';
|
||||||
export * from './client-hook';
|
export * from './client-hook';
|
||||||
|
export * from './lazy-request-hook';
|
||||||
export * from './request-hook';
|
export * from './request-hook';
|
||||||
export * from './request-context';
|
export * from './request-context';
|
||||||
export * from './reducer';
|
export * from './reducer';
|
||||||
|
|
115
src/lazy-request-hook.ts
Normal file
115
src/lazy-request-hook.ts
Normal 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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ export type RequestState<R> = Readonly<{
|
||||||
prevParams?: Record<string, any>
|
prevParams?: Record<string, any>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
export type PublicRequestState<R> = Pick<RequestState<R>, 'data' | 'loading' | 'isCalled'>;
|
||||||
|
|
||||||
export type RequestAction<R> =
|
export type RequestAction<R> =
|
||||||
| { type: 'call', headers: Record<string, string>, variables: Record<string, any>, params?: Record<string, any> }
|
| { type: 'call', headers: Record<string, string>, variables: Record<string, any>, params?: Record<string, any> }
|
||||||
| { type: 'success', response: ClientResponse<R> }
|
| { type: 'success', response: ClientResponse<R> }
|
||||||
|
|
|
@ -1,111 +1,35 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import invariant from 'tiny-invariant';
|
import invariant from 'tiny-invariant';
|
||||||
import isEqual from 'lodash.isequal';
|
import { Endpoint, Method } from './endpoint';
|
||||||
import { useClient } from './client-hook';
|
import { LazyRequestConfig, useLazyRequest } from './lazy-request-hook';
|
||||||
import { Endpoint } from './endpoint';
|
|
||||||
import { RequestAction, requestReducer, RequestState } from './reducer';
|
|
||||||
import { useRequestContext } from './request-context';
|
|
||||||
|
|
||||||
export type RequestConfig<R, V, P = void> = Readonly<{
|
export type RequestConfig<R, V, P> = Readonly<
|
||||||
variables?: V;
|
LazyRequestConfig<R, V, P>
|
||||||
params?: P;
|
& {
|
||||||
headers?: Record<string, string>;
|
skip?: boolean,
|
||||||
onComplete?: (data: R) => unknown;
|
}
|
||||||
}>
|
|
||||||
|
|
||||||
export type RequestHandlerConfig<R, V, P> = Readonly<
|
|
||||||
RequestConfig<R, V, P>
|
|
||||||
& { force?: 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>(
|
export function useRequest<R = Record<string, any>, V = Record<string, any>, P = void>(
|
||||||
endpoint: Endpoint<P>,
|
endpoint: Endpoint<P>,
|
||||||
config?: RequestConfig<R, V, P>,
|
config?: RequestConfig<R, V, P>,
|
||||||
): [RequestHandler<R, V, P>, RequestState<R>] {
|
) {
|
||||||
const [client] = useClient();
|
invariant(
|
||||||
const { defaultHeaders } = useRequestContext();
|
endpoint.method == Method.GET,
|
||||||
const [state, dispatch] = React.useReducer<React.Reducer<RequestState<R>, RequestAction<R>>>(
|
`You cannot use useRequest with ${endpoint.method} method`
|
||||||
requestReducer,
|
|
||||||
{
|
|
||||||
data: null,
|
|
||||||
loading: false,
|
|
||||||
isCalled: false,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handler = React.useCallback(
|
const [handler, state] = useLazyRequest(endpoint, config);
|
||||||
(handlerConfig?: RequestHandlerConfig<R, V, P>) => {
|
const skip = React.useMemo(() => config?.skip ?? false, [config]);
|
||||||
if (state?.loading) {
|
|
||||||
return Promise.resolve(null);
|
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 [
|
return state;
|
||||||
handler,
|
|
||||||
state,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue