feat!(request-hook): add new request hook
This hook works only for endpoint with GET method. * refac!(request-hook): add lazy prefix to request hook BREAKING CHANGES: you need to rename all `useRequest` hooks to `useLazyRequest` * refac!(request-hook): add public request state BREAKING CHANGES: User shouldn't see previous headers, variables and params. It's only for hooks so it doesn't call request again. If you use these state you should to remove it from your code. * chore: update example Closes #5
This commit is contained in:
parent
9b9150524c
commit
8c6833805b
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