feat!: transform endpoint data before state

BREAKING CHANGES: Endpoint now required response, variables and
params types

Closes #3
This commit is contained in:
Dmitriy Pleshevskiy 2020-11-20 20:01:24 +03:00
parent b944725e07
commit c848b0c785
7 changed files with 82 additions and 22 deletions

View file

@ -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[],
}
```

View file

@ -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}`,
};

View file

@ -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() {

View file

@ -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) => {

View file

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

View file

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

View file

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