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