Merge pull request #6 from pleshevskiy/task-2
feat!(client): add client fetch based breaking!(context): client prop instead base url refac: change axios response to client response chore(deps): remove axios chore: update example
This commit is contained in:
commit
0c58bcc036
15 changed files with 136 additions and 81 deletions
|
@ -15,9 +15,11 @@ npm install react-rest-request --save
|
|||
```typescript
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Endpoint, Method, useRequest, RequestProvider } from 'react-rest-request';
|
||||
import { Client, Endpoint, Method, useRequest, RequestProvider } from 'react-rest-request';
|
||||
|
||||
const BASE_API_URL = 'https://sampleapis.com/movies/api';
|
||||
const client = Client({
|
||||
baseUrl: 'https://sampleapis.com/movies/api',
|
||||
});
|
||||
|
||||
type Movie = Readonly<{
|
||||
id: number;
|
||||
|
@ -55,7 +57,7 @@ function App() {
|
|||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<RequestProvider baseUrl={BASE_API_URL}>
|
||||
<RequestProvider client={client}>
|
||||
<App />
|
||||
</RequestProvider>,
|
||||
document.getElementById('root'),
|
||||
|
|
41
examples/movies/package-lock.json
generated
41
examples/movies/package-lock.json
generated
|
@ -12381,11 +12381,6 @@
|
|||
"react-rest-request": {
|
||||
"version": "file:../..",
|
||||
"dependencies": {
|
||||
"color-convert": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
|
||||
"integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ=="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
|
@ -12408,11 +12403,6 @@
|
|||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
},
|
||||
"fastq": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz",
|
||||
"integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q=="
|
||||
},
|
||||
"globals": {
|
||||
"version": "12.4.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
|
||||
|
@ -12428,11 +12418,6 @@
|
|||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
|
||||
},
|
||||
"run-parallel": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
|
||||
"integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
@ -12445,26 +12430,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz",
|
||||
"integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg=="
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -14879,6 +14844,12 @@
|
|||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz",
|
||||
"integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"unicode-canonical-property-names-ecmascript": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-dom": "^16.9.9"
|
||||
"@types/react-dom": "^16.9.9",
|
||||
"typescript": "^4.0.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export const BASE_API_URL = 'https://sampleapis.com/movies/api';
|
|
@ -1,12 +1,15 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { RequestProvider } from 'react-rest-request';
|
||||
import { BASE_API_URL } from './constants';
|
||||
import { RequestProvider, Client } from 'react-rest-request';
|
||||
import App from './app';
|
||||
|
||||
const client = new Client({
|
||||
baseUrl: 'https://sampleapis.com/movies/api',
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<RequestProvider baseUrl={BASE_API_URL}>
|
||||
<RequestProvider client={client}>
|
||||
<App />
|
||||
</RequestProvider>
|
||||
</React.StrictMode>,
|
||||
|
|
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -520,14 +520,6 @@
|
|||
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz",
|
||||
"integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
|
@ -1115,11 +1107,6 @@
|
|||
"integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
|
||||
"dev": true
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
|
||||
"integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
},
|
||||
"homepage": "https://github.com/pleshevskiy/react-rest-request#readme",
|
||||
"dependencies": {
|
||||
"axios": "^0.20.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"react": "^17.0.1",
|
||||
"tiny-invariant": "^1.1.0"
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
import Axios from 'axios';
|
||||
import React from 'react';
|
||||
import { useRequestContext } from './request-context';
|
||||
|
||||
export function useClient() {
|
||||
const { baseUrl } = useRequestContext();
|
||||
|
||||
const client = React.useMemo(
|
||||
() => Axios.create({ baseURL: baseUrl }),
|
||||
[baseUrl]
|
||||
);
|
||||
const { client } = useRequestContext();
|
||||
|
||||
return [client];
|
||||
}
|
||||
|
|
71
src/client.ts
Normal file
71
src/client.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import invariant from 'tiny-invariant';
|
||||
import { Method } from './endpoint';
|
||||
import { formDataFromObject, urlSearchParamsFromObject } from './misc';
|
||||
|
||||
export type ClientConfig = {
|
||||
baseUrl: string,
|
||||
}
|
||||
|
||||
export type RequestProps = {
|
||||
url: string,
|
||||
method: Method,
|
||||
headers: Record<string, string>,
|
||||
variables: Record<string, any> | FormData,
|
||||
}
|
||||
|
||||
export type ClientResponse<Data extends Record<string, any>> = Readonly<Response & {
|
||||
data: Data;
|
||||
}>
|
||||
|
||||
export class Client {
|
||||
constructor(private config: ClientConfig) {}
|
||||
|
||||
private prepareRequest(props: RequestProps) {
|
||||
const requestCanContainBody = [Method.POST, Method.PATCH, Method.PUT].includes(props.method);
|
||||
|
||||
const url = /https?:\/\//.test(props.url) ?
|
||||
new URL(props.url)
|
||||
: new URL(this.config.baseUrl + props.url);
|
||||
if (!requestCanContainBody) {
|
||||
invariant(!(props.variables instanceof FormData), `Method ${props.method} cannot contain body`);
|
||||
|
||||
url.search = urlSearchParamsFromObject(props.variables).toString();
|
||||
}
|
||||
|
||||
const headers = new Headers(props.headers);
|
||||
if (requestCanContainBody && !headers.has('content-type')) {
|
||||
headers.set('content-type', 'application/json');
|
||||
}
|
||||
|
||||
const contentType = headers.get('content-type');
|
||||
const body = !requestCanContainBody ? (
|
||||
undefined
|
||||
) : contentType === 'application/json' ? (
|
||||
JSON.stringify(props.variables)
|
||||
) : contentType === 'multipart/form-data' ? (
|
||||
props.variables instanceof FormData ? (
|
||||
props.variables
|
||||
) : (
|
||||
formDataFromObject(props.variables)
|
||||
)
|
||||
) : (
|
||||
/* TODO: need to add more content-type of body */
|
||||
undefined
|
||||
);
|
||||
|
||||
return new Request(url.toString(), {
|
||||
headers,
|
||||
method: props.method,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
public request<Data extends Record<string, any>>(props: RequestProps): Promise<ClientResponse<Data>> {
|
||||
const req = this.prepareRequest(props);
|
||||
|
||||
return fetch(req)
|
||||
// TODO: need to check response headers and parse json only if content-type header is application/json
|
||||
.then(res => Promise.all([res, res.json()]))
|
||||
.then(([res, data]) => ({ ...res, data }));
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
export * from './endpoint';
|
||||
export * from './client';
|
||||
export * from './client-hook';
|
||||
export * from './request-hook';
|
||||
export * from './request-context';
|
||||
|
|
33
src/misc.ts
Normal file
33
src/misc.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
|
||||
export function isObject(val: any) {
|
||||
return Object.prototype.toString.call(val) === '[object Object]';
|
||||
}
|
||||
|
||||
export function formDataFromObject(obj: Record<string, any>) {
|
||||
const formData = new FormData();
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(val => formData.append(key, val));
|
||||
} else if (isObject(value)) {
|
||||
formData.set(key, JSON.stringify(value));
|
||||
} else {
|
||||
formData.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
export function urlSearchParamsFromObject(obj: Record<string, any>) {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (Array.isArray(value)) {
|
||||
const arrayKey = `${key}[]`;
|
||||
value.forEach(val => searchParams.append(arrayKey, val));
|
||||
} else {
|
||||
searchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return searchParams;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { AxiosResponse } from 'axios';
|
||||
import { ClientResponse } from './client';
|
||||
|
||||
export type RequestState<R> = Readonly<{
|
||||
data: R | null;
|
||||
|
@ -11,8 +11,8 @@ export type RequestState<R> = Readonly<{
|
|||
|
||||
export type RequestAction<R> =
|
||||
| { type: 'call', headers: Record<string, string>, variables: Record<string, any>, params?: Record<string, any> }
|
||||
| { type: 'success', response: AxiosResponse<R> }
|
||||
| { type: 'failure', response: AxiosResponse<R> }
|
||||
| { type: 'success', response: ClientResponse<R> }
|
||||
| { type: 'failure', response: ClientResponse<R> }
|
||||
|
||||
export function requestReducer<R>(state: RequestState<R>, action: RequestAction<R>) {
|
||||
switch (action.type) {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { Client } from './client';
|
||||
|
||||
|
||||
export type RequestContextData = Readonly<{
|
||||
baseUrl: string;
|
||||
client: Client;
|
||||
defaultHeaders?: Record<string, string>;
|
||||
}>
|
||||
|
||||
|
@ -11,9 +12,9 @@ const RequestContext = React.createContext<RequestContextData | null>(null);
|
|||
|
||||
export type RequestProviderProps = Readonly<React.PropsWithChildren<RequestContextData>>
|
||||
|
||||
export function RequestProvider({ baseUrl, defaultHeaders, children }: RequestProviderProps) {
|
||||
export function RequestProvider({ client, defaultHeaders, children }: RequestProviderProps) {
|
||||
return (
|
||||
<RequestContext.Provider value={{ baseUrl, defaultHeaders }}>
|
||||
<RequestContext.Provider value={{ client, defaultHeaders }}>
|
||||
{children}
|
||||
</RequestContext.Provider>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import invariant from 'tiny-invariant';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { useClient } from './client-hook';
|
||||
import { Endpoint, Method } from './endpoint';
|
||||
import { Endpoint } from './endpoint';
|
||||
import { RequestAction, requestReducer, RequestState } from './reducer';
|
||||
import { useRequestContext } from './request-context';
|
||||
|
||||
|
@ -35,8 +35,6 @@ export function useRequest<R = Record<string, any>, V = Record<string, any>, P =
|
|||
}
|
||||
);
|
||||
|
||||
const variableParam = requestVariableParamByMethod(endpoint.method);
|
||||
|
||||
const handler = React.useCallback(
|
||||
(handlerConfig?: RequestHandlerConfig<R, V, P>) => {
|
||||
if (state?.loading) {
|
||||
|
@ -87,7 +85,7 @@ export function useRequest<R = Record<string, any>, V = Record<string, any>, P =
|
|||
...endpoint,
|
||||
url: endpointUrl,
|
||||
headers,
|
||||
[variableParam]: variables,
|
||||
variables,
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
|
@ -103,7 +101,7 @@ export function useRequest<R = Record<string, any>, V = Record<string, any>, P =
|
|||
}
|
||||
);
|
||||
},
|
||||
[state, config, client, endpoint, variableParam, defaultHeaders]
|
||||
[state, config, client, endpoint, defaultHeaders]
|
||||
);
|
||||
|
||||
return [
|
||||
|
@ -111,7 +109,3 @@ export function useRequest<R = Record<string, any>, V = Record<string, any>, P =
|
|||
state,
|
||||
];
|
||||
}
|
||||
|
||||
function requestVariableParamByMethod(method: Method) {
|
||||
return [Method.GET, Method.DELETE].includes(method) ? 'params' : 'data';
|
||||
}
|
|
@ -17,6 +17,6 @@
|
|||
"declaration": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
"src"
|
||||
],
|
||||
}
|
||||
|
|
Reference in a new issue