From 8c6833805ba03aec30fb06e9b4d649cf4426493a Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Thu, 5 Nov 2020 22:33:06 +0300 Subject: [PATCH] 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 --- examples/movies/src/app.tsx | 9 +-- package-lock.json | 2 +- src/index.ts | 1 + src/lazy-request-hook.ts | 115 +++++++++++++++++++++++++++++++++++ src/reducer.ts | 2 + src/request-hook.ts | 116 +++++++----------------------------- 6 files changed, 140 insertions(+), 105 deletions(-) create mode 100644 src/lazy-request-hook.ts diff --git a/examples/movies/src/app.tsx b/examples/movies/src/app.tsx index 25ac5b3..f49d59a 100644 --- a/examples/movies/src/app.tsx +++ b/examples/movies/src/app.tsx @@ -3,14 +3,7 @@ import { useRequest } from 'react-rest-request'; import { MoviesEndpoint, MoviesResponse } from './endpoint'; export default function App() { - const [movies, { data, loading }] = useRequest(MoviesEndpoint); - - React.useEffect( - () => { - movies(); - }, - [movies] - ); + const { data, loading } = useRequest(MoviesEndpoint); return !data ? (
{ loading ? 'Loading...' : 'Something went wrong' }
diff --git a/package-lock.json b/package-lock.json index 7a690a8..25a6c2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-rest-request", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/index.ts b/src/index.ts index fcabddc..de1a6dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/lazy-request-hook.ts b/src/lazy-request-hook.ts new file mode 100644 index 0000000..1ee4281 --- /dev/null +++ b/src/lazy-request-hook.ts @@ -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 = Readonly<{ + variables?: V; + params?: P; + headers?: Record; + onComplete?: (data: R) => unknown; +}> + +export type LazyRequestHandlerConfig = Readonly< + LazyRequestConfig + & { force?: boolean } +> + +export type RequestHandler = (config?: LazyRequestHandlerConfig) => Promise; + +export function useLazyRequest, V = Record, P = void>( + endpoint: Endpoint

, + config?: LazyRequestConfig, +): [RequestHandler, PublicRequestState] { + const [client] = useClient(); + const { defaultHeaders } = useRequestContext(); + const [state, dispatch] = React.useReducer, RequestAction>>( + requestReducer, + { + data: null, + loading: false, + isCalled: false, + } + ); + + const handler = React.useCallback( + (handlerConfig?: LazyRequestHandlerConfig) => { + 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({ + ...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, + }, + ]; +} diff --git a/src/reducer.ts b/src/reducer.ts index e4d2989..1511acc 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -9,6 +9,8 @@ export type RequestState = Readonly<{ prevParams?: Record }> +export type PublicRequestState = Pick, 'data' | 'loading' | 'isCalled'>; + export type RequestAction = | { type: 'call', headers: Record, variables: Record, params?: Record } | { type: 'success', response: ClientResponse } diff --git a/src/request-hook.ts b/src/request-hook.ts index 2ae071c..bc292d1 100644 --- a/src/request-hook.ts +++ b/src/request-hook.ts @@ -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 = Readonly<{ - variables?: V; - params?: P; - headers?: Record; - onComplete?: (data: R) => unknown; -}> - -export type RequestHandlerConfig = Readonly< - RequestConfig - & { force?: boolean } +export type RequestConfig = Readonly< + LazyRequestConfig + & { + skip?: boolean, + } > -export type RequestHandler = (config?: RequestHandlerConfig) => Promise; - export function useRequest, V = Record, P = void>( endpoint: Endpoint

, config?: RequestConfig, -): [RequestHandler, RequestState] { - const [client] = useClient(); - const { defaultHeaders } = useRequestContext(); - const [state, dispatch] = React.useReducer, RequestAction>>( - 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) => { - 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({ - ...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; }