Initial commit

feat(context): add context for request
feat(client): add hook for get memo rest client
feat(endpoint): add endpoint object for configure rest methods
feat(request): add request hook
refac(request): return cached data for same query
refac(request): remove void variables for endpoints
feat(request): add headers to request cache
This commit is contained in:
Dmitriy Pleshevskiy 2019-09-21 07:59:18 +03:00
commit e9c18c9fea
14 changed files with 3021 additions and 0 deletions

7
.dockerignore Normal file
View file

@ -0,0 +1,7 @@
.idea/
.vscode/
node_modules/
target/

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
*.js

49
.eslintrc.yml Normal file
View file

@ -0,0 +1,49 @@
env:
es6: true
node: true
browser: true
parser: '@typescript-eslint/parser'
plugins:
- 'react-hooks'
extends:
- 'plugin:react/recommended'
- 'plugin:react-hooks/recommended'
- 'plugin:@typescript-eslint/recommended'
parserOptions:
ecmaVersion: 2020
sourceType: 'module'
ecmaFeatures:
jsx: true
settings:
react:
version: 'detect'
rules:
no-case-declarations: off
linebreak-style: off
indent:
- error
- 4
- { "SwitchCase": 1 }
quotes:
- error
- single
semi:
- error
- always
'@typescript-eslint/no-unused-vars':
- warn
- vars: all
args: after-used
argsIgnorePattern: ^_
varsIgnorePattern: ^_
ignoreRestSiblings: true
'@typescript-eslint/no-empty-interface': off
'@typescript-eslint/no-explicit-any': off
'@typescript-eslint/no-extra-semi': off
'@typescript-eslint/explicit-function-return-type': off
'@typescript-eslint/explicit-module-boundary-types': off
'@typescript-eslint/camelcase': off
'@typescript-eslint/no-use-before-define': off
'react/prop-types': off
'react/display-name': off

20
.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
.idea/
.vscode/
# dependencies
/node_modules
# testing
/coverage
# production
/target
/public
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*

10
README.md Normal file
View file

@ -0,0 +1,10 @@
# react-rest-request
Minimalistic REST API client for React inspired by Apollo.
# Installation
```bash
npm install react-rest-request
```

2641
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

47
package.json Normal file
View file

@ -0,0 +1,47 @@
{
"name": "react-rest-request",
"version": "0.1.0",
"description": "Minimalistic REST API client for React inspired by Apollo",
"readme": "README.md",
"main": "./target/index.js",
"types": "./target/index.d.ts",
"files": [
"target"
],
"scripts": {
"prepublishOnly": "tsc"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pleshevskiy/react-rest-request.git"
},
"keywords": [
"react",
"rest",
"request",
"fetch",
"hook"
],
"author": "Dmitriy Pleshevskiy <dmitriy@ideascup.me>",
"license": "MIT",
"bugs": {
"url": "https://github.com/pleshevskiy/react-rest-request/issues"
},
"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"
},
"devDependencies": {
"@types/lodash.isequal": "^4.5.5",
"@types/react": "^16.9.55",
"@typescript-eslint/eslint-plugin": "^4.6.1",
"@typescript-eslint/parser": "^4.6.1",
"eslint": "^7.12.1",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
"typescript": "^4.0.5"
}
}

14
src/client-hook.ts Normal file
View file

@ -0,0 +1,14 @@
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]
);
return [client];
}

14
src/endpoint.ts Normal file
View file

@ -0,0 +1,14 @@
export enum Method {
GET = 'GET',
PUT = 'PUT',
POST = 'POST',
PATCH = 'PATCH',
DELETE = 'DELETE',
}
export type Endpoint<P = void> = Readonly<{
method: Method;
url: string | ((params: P) => string);
headers?: Record<string, string>;
}>

5
src/index.ts Normal file
View file

@ -0,0 +1,5 @@
export * from './endpoint';
export * from './client-hook';
export * from './request-hook';
export * from './request-context';
export * from './reducer';

45
src/reducer.ts Normal file
View file

@ -0,0 +1,45 @@
import { AxiosResponse } from 'axios';
export type RequestState<R> = Readonly<{
data: R | null;
loading: boolean;
isCalled: boolean;
prevHeaders?: Record<string, string>;
prevVariables?: Record<string, any>;
prevParams?: Record<string, any>
}>
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> }
export function requestReducer<R>(state: RequestState<R>, action: RequestAction<R>) {
switch (action.type) {
case 'call': {
return {
...state,
loading: true,
isCalled: true,
prevHeaders: action.headers,
prevVariables: action.variables,
prevParams: action.params,
};
}
case 'success': {
return {
...state,
loading: false,
data: action.response.data,
};
}
case 'failure': {
return {
...state,
loading: false,
data: null,
// TODO: need to append errors
};
}
}
}

29
src/request-context.tsx Normal file
View file

@ -0,0 +1,29 @@
import React from 'react';
import invariant from 'tiny-invariant';
export type RequestContextData = Readonly<{
baseUrl: string;
defaultHeaders?: Record<string, string>;
}>
const RequestContext = React.createContext<RequestContextData | null>(null);
export type RequestProviderProps = Readonly<React.PropsWithChildren<RequestContextData>>
export function RequestProvider({ baseUrl, defaultHeaders, children }: RequestProviderProps) {
return (
<RequestContext.Provider value={{ baseUrl, defaultHeaders }}>
{children}
</RequestContext.Provider>
);
}
export function useRequestContext() {
const context = React.useContext(RequestContext);
invariant(context, 'useRequestContext() must be a child of <RequestProvider />');
return context;
}

117
src/request-hook.ts Normal file
View file

@ -0,0 +1,117 @@
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 { RequestAction, requestReducer, RequestState } from './reducer';
import { useRequestContext } from './request-context';
export type RequestConfig<R, V, P = void> = Readonly<{
variables?: V;
params?: P;
headers?: Record<string, string>;
onComplete?: (data: R) => unknown;
}>
export type RequestHandlerConfig<R, V, P> = Readonly<
RequestConfig<R, V, P>
& { force?: boolean }
>
export type RequestHandler<R, V, P> = (config?: RequestHandlerConfig<R, V, P>) => Promise<R | null>;
export function useRequest<R = Record<string, any>, V = Record<string, any>, P = void>(
endpoint: Endpoint<P>,
config?: RequestConfig<R, V, P>,
): [RequestHandler<R, V, P>, RequestState<R>] {
const [client] = useClient();
const { defaultHeaders } = useRequestContext();
const [state, dispatch] = React.useReducer<React.Reducer<RequestState<R>, RequestAction<R>>>(
requestReducer,
{
data: null,
loading: false,
isCalled: false,
}
);
const variableParam = requestVariableParamByMethod(endpoint.method);
const handler = React.useCallback(
(handlerConfig?: RequestHandlerConfig<R, V, P>) => {
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<R>({
...endpoint,
url: endpointUrl,
headers,
[variableParam]: 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, variableParam, defaultHeaders]
);
return [
handler,
state,
];
}
function requestVariableParamByMethod(method: Method) {
return [Method.GET, Method.DELETE].includes(method) ? 'params' : 'data';
}

22
tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"rootDir": "src",
"outDir": "target",
"module": "esnext",
"target": "es6",
"lib": ["dom", "es2020"],
"jsx": "react",
"moduleResolution": "node",
"pretty": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strict": true,
"strictNullChecks": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
},
"include": [
"src/**/*"
],
}