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:
Dmitriy Pleshevskiy 2020-11-05 22:33:06 +03:00
parent 9b9150524c
commit 8c6833805b
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'; 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
View file

@ -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": {

View file

@ -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
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> 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> }

View file

@ -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) => { [skip, handler]
dispatch({ type: 'failure', response });
throw response;
}
);
},
[state, config, client, endpoint, defaultHeaders]
); );
return [ return state;
handler,
state,
];
} }