feat!: transform endpoint data before state
BREAKING CHANGES: Endpoint now required response, variables and params types Closes #3
This commit is contained in:
parent
b944725e07
commit
c848b0c785
7 changed files with 82 additions and 22 deletions
33
README.md
33
README.md
|
@ -28,7 +28,7 @@ type Movie = Readonly<{
|
|||
imdbId: string;
|
||||
}>
|
||||
|
||||
const MoviesEndpoint: Endpoint = {
|
||||
const MoviesEndpoint: Endpoint<MoviesResponse, void> = {
|
||||
method: Method.GET,
|
||||
url: '/action-adventure',
|
||||
};
|
||||
|
@ -56,3 +56,34 @@ ReactDOM.render(
|
|||
document.getElementById('root'),
|
||||
);
|
||||
```
|
||||
|
||||
### Transform response
|
||||
|
||||
If you have an endpoint that doesn't fit into your beautiful architecture
|
||||
with its response data, you can transform the response before it's written
|
||||
to the state.
|
||||
|
||||
```typescript
|
||||
import { Endpoint, Method } from 'react-rest-request';
|
||||
|
||||
export type Movie = Readonly<{
|
||||
id: number;
|
||||
title: string;
|
||||
posterURL: string;
|
||||
imdbId: string;
|
||||
}>
|
||||
|
||||
export const MoviesEndpoint: Endpoint<MoviesResponse, void> = {
|
||||
method: Method.GET,
|
||||
url: '/action-adventure',
|
||||
transformResponseData(data: Movie[]) {
|
||||
return {
|
||||
items: data,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export type MoviesResponse = {
|
||||
items: Movie[],
|
||||
}
|
||||
```
|
||||
|
|
|
@ -7,15 +7,21 @@ export type Movie = Readonly<{
|
|||
imdbId: string;
|
||||
}>
|
||||
|
||||
export const MoviesEndpoint: Endpoint = {
|
||||
export const MoviesEndpoint: Endpoint<MoviesResponse, void> = {
|
||||
method: Method.GET,
|
||||
url: '/action-adventure',
|
||||
transformResponseData(data: Movie[]) {
|
||||
return {
|
||||
items: data,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export type MoviesResponse = Movie[];
|
||||
export type MoviesResponse = {
|
||||
items: Movie[],
|
||||
}
|
||||
|
||||
|
||||
export const MovieEndpoint: Endpoint<MovieParams> = {
|
||||
export const MovieEndpoint: Endpoint<MovieResponse, void, MovieParams> = {
|
||||
method: Method.GET,
|
||||
url: ({ id }) => `/action-adventure/${id}`,
|
||||
};
|
||||
|
|
|
@ -9,9 +9,9 @@ export function MoviesPage() {
|
|||
|
||||
return !data ? (
|
||||
<div>{ loading ? 'Loading...' : 'Something went wrong' }</div>
|
||||
) : Array.isArray(data) ? (
|
||||
) : (
|
||||
<ul>
|
||||
{data
|
||||
{data.items
|
||||
.filter(movie => !!movie.title)
|
||||
.map(movie => (
|
||||
<li key={movie.id}>
|
||||
|
@ -22,7 +22,7 @@ export function MoviesPage() {
|
|||
<Link to='/9999'><span style={{color: 'red'}}>NOT EXIST</span></Link>
|
||||
</li>
|
||||
</ul>
|
||||
) : <div>{JSON.stringify(data)}</div>;
|
||||
);
|
||||
}
|
||||
|
||||
export function MoviePage() {
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import invariant from 'tiny-invariant';
|
||||
import { Method } from './endpoint';
|
||||
import { formDataFromObject, urlSearchParamsFromObject } from './misc';
|
||||
import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './misc';
|
||||
|
||||
export type ClientConfig = {
|
||||
baseUrl: string,
|
||||
}
|
||||
|
||||
export type RequestProps = {
|
||||
type PrepareRequestProps = {
|
||||
url: string,
|
||||
method: Method,
|
||||
headers: Record<string, string>,
|
||||
variables: Record<string, any> | FormData,
|
||||
}
|
||||
|
||||
export type RequestProps<R> = PrepareRequestProps & {
|
||||
transformResponseData?: (data: unknown) => R,
|
||||
}
|
||||
|
||||
export type ClientResponse<Data extends Record<string, any>> = Readonly<
|
||||
Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'>
|
||||
& { data: Data }
|
||||
|
@ -21,7 +25,7 @@ export type ClientResponse<Data extends Record<string, any>> = Readonly<
|
|||
export class Client {
|
||||
constructor(private config: ClientConfig) {}
|
||||
|
||||
private prepareRequest(props: RequestProps) {
|
||||
private prepareRequest(props: PrepareRequestProps) {
|
||||
const requestCanContainBody = [Method.POST, Method.PATCH, Method.PUT].includes(props.method);
|
||||
|
||||
const url = /https?:\/\//.test(props.url) ?
|
||||
|
@ -61,8 +65,13 @@ export class Client {
|
|||
});
|
||||
}
|
||||
|
||||
public request<Data extends Record<string, any>>(props: RequestProps): Promise<ClientResponse<Data>> {
|
||||
const req = this.prepareRequest(props);
|
||||
public request<Data extends Record<string, any>>(
|
||||
{
|
||||
transformResponseData,
|
||||
...restProps
|
||||
}: RequestProps<Data>
|
||||
): Promise<ClientResponse<Data>> {
|
||||
const req = this.prepareRequest(restProps);
|
||||
|
||||
return fetch(req)
|
||||
// TODO: need to check response headers and parse json only if content-type header is application/json
|
||||
|
@ -76,7 +85,7 @@ export class Client {
|
|||
type: res.type,
|
||||
headers: res.headers,
|
||||
url: res.url,
|
||||
data
|
||||
data: isFunction(transformResponseData) ? transformResponseData(data) : data,
|
||||
};
|
||||
})
|
||||
.then((res) => {
|
||||
|
|
|
@ -8,8 +8,9 @@ export enum Method {
|
|||
DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
export type Endpoint<P = void> = Readonly<{
|
||||
export type Endpoint<R, V, P = void> = Readonly<{
|
||||
method: Method;
|
||||
url: string | ((params: P) => string);
|
||||
headers?: Record<string, string>;
|
||||
transformResponseData?: (data: any) => R;
|
||||
}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import invariant from 'tiny-invariant';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { useClient } from './client-hook';
|
||||
|
@ -6,6 +6,7 @@ import { Endpoint } from './endpoint';
|
|||
import { PublicRequestState, RequestAction, requestReducer, RequestState } from './reducer';
|
||||
import { useRequestContext } from './request-context';
|
||||
import { ClientResponse } from './client';
|
||||
import { isFunction } from './misc';
|
||||
|
||||
export type LazyRequestConfig<R, V, P = void> = Readonly<{
|
||||
variables?: V;
|
||||
|
@ -23,7 +24,7 @@ export type LazyRequestHandlerConfig<R, V, P> = Readonly<
|
|||
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>,
|
||||
endpoint: Endpoint<R, V, P>,
|
||||
config?: LazyRequestConfig<R, V, P>,
|
||||
): [RequestHandler<R, V, P>, PublicRequestState<R>] {
|
||||
const [client] = useClient();
|
||||
|
@ -37,6 +38,15 @@ export function useLazyRequest<R = Record<string, any>, V = Record<string, any>,
|
|||
}
|
||||
);
|
||||
|
||||
const transformResponseData = useCallback(
|
||||
(data: unknown): R => {
|
||||
return isFunction(endpoint.transformResponseData) ?
|
||||
endpoint.transformResponseData(data)
|
||||
: data as R;
|
||||
},
|
||||
[endpoint]
|
||||
);
|
||||
|
||||
const handler = React.useCallback(
|
||||
(handlerConfig?: LazyRequestHandlerConfig<R, V, P>) => {
|
||||
if (state?.loading) {
|
||||
|
@ -46,7 +56,7 @@ export function useLazyRequest<R = Record<string, any>, V = Record<string, any>,
|
|||
let params: P | undefined;
|
||||
let endpointUrl: string;
|
||||
let isSameRequest = true;
|
||||
if (typeof endpoint.url === 'function') {
|
||||
if (isFunction(endpoint.url)) {
|
||||
params = handlerConfig?.params ?? config?.params;
|
||||
invariant(params, 'Endpoind required params');
|
||||
|
||||
|
@ -89,25 +99,28 @@ export function useLazyRequest<R = Record<string, any>, V = Record<string, any>,
|
|||
url: endpointUrl,
|
||||
headers,
|
||||
variables,
|
||||
transformResponseData,
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
dispatch({ type: 'success', response });
|
||||
if (typeof onComplete === 'function') {
|
||||
|
||||
if (isFunction(onComplete)) {
|
||||
onComplete(response.data);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
},
|
||||
(response) => {
|
||||
dispatch({ type: 'failure', response });
|
||||
if (typeof onFailure === 'function') {
|
||||
if (isFunction(onFailure)) {
|
||||
onFailure(response);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
},
|
||||
[state, config, client, endpoint, defaultHeaders]
|
||||
[state, config, client, endpoint, defaultHeaders, transformResponseData]
|
||||
);
|
||||
|
||||
return [
|
||||
|
|
|
@ -11,7 +11,7 @@ export type RequestConfig<R, V, P> = Readonly<
|
|||
>
|
||||
|
||||
export function useRequest<R = Record<string, any>, V = Record<string, any>, P = void>(
|
||||
endpoint: Endpoint<P>,
|
||||
endpoint: Endpoint<R, V, P>,
|
||||
config?: RequestConfig<R, V, P>,
|
||||
) {
|
||||
invariant(
|
||||
|
|
Reference in a new issue