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';
|
||||
|
||||
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
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-rest-request",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -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
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>
|
||||
}>
|
||||
|
||||
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> }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Reference in a new issue