├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── example
├── .npmignore
├── axios.ts
├── index.html
├── index.tsx
├── package.json
├── tsconfig.json
└── yarn.lock
├── package.json
├── src
├── index.ts
└── types.ts
├── test
└── blah.test.tsx
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | .rts2_cache_cjs
6 | .rts2_cache_esm
7 | .rts2_cache_umd
8 | dist
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 H2rmone
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # use-request
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | A Tiny Custom React hooks for making request.
12 |
13 | ## Feature
14 |
15 | - 👕 Typescript support.
16 | - 🗜️ 1.3kb after minified without gzip.
17 | - 📤 Easy to use with different request client.
18 |
19 | ## Install
20 |
21 | ```bash
22 | yarn add @rekindle/use-request
23 | ```
24 |
25 | ## Basic Usage
26 |
27 | ```jsx
28 | import useRequest from '@rekindle/use-request'
29 | import getFooApi from '@/utils/axios'
30 |
31 | function App () {
32 | const [{ loading, error, data }, fetch] = useRequest(getFooApi)
33 |
34 | useEffect(() => { fetch() }, [fetch])
35 |
36 | if (loading) return 'loading'
37 | if (error) return 'error'
38 |
39 | return data &&
{data}
40 | }
41 | ```
42 |
43 | ## More Example
44 |
45 | > Queries are typically start with loading, we can create a `useQuery` function before we use.
46 |
47 | ```js
48 | function useQuery (api) {
49 | return useRequest(api, { loading: true })
50 | }
51 | ```
52 |
53 | ### Query
54 |
55 | ```jsx
56 | function Query () {
57 | const [{ loading, error, data }, fetch] = useQuery(queryFooApi)
58 |
59 | useEffect(() => { fetch() }, [fetch])
60 |
61 | if (loading) return 'loading...'
62 | if (error) return 'error'
63 |
64 | // no need to check data
65 | return {data}
66 | }
67 | ```
68 |
69 | ### Multi Query
70 |
71 | ```jsx
72 | function MultiQuery () {
73 | const [foo, fetchFoo] = useQuery(queryFooApi)
74 | const [bar, fetchBar] = useQuery(queryBarApi)
75 |
76 | useEffect(() => {
77 | fetchFoo()
78 | fetchBar()
79 | }, [fetchFoo, fetchBar])
80 |
81 | const renderContent = (state) => {
82 | const { loading, error, data } = state
83 |
84 | if (loading) return 'loading...'
85 | if (error) return 'error'
86 |
87 | return {data}
88 | }
89 |
90 | return (
91 |
92 | {renderContent(foo)}
93 | {renderContent(bar)}
94 |
95 | )
96 | }
97 | ```
98 |
99 | ### Batch Query
100 |
101 | ```jsx
102 | function BatchQuery () {
103 | const batchFetchApi = () => Promise.all([queryFooApi(), queryBarApi()])
104 | const [{ loading, error, data }, fetch] = useQuery(batchFetchApi)
105 |
106 | useEffect(() => { fetch() }, [fetch])
107 |
108 | if (loading) return 'loading...'
109 | if (error) return 'error'
110 |
111 | const [foo, bar] = data
112 |
113 | return (
114 |
115 |
{foo}
116 |
{bar}
117 |
refetch batch
118 |
119 | )
120 | }
121 | ```
122 |
123 | ### Mutate
124 |
125 | ```jsx
126 | function Mutate () {
127 | const [{ loading, error, data }, mutate] = useRequest(mutateBazApi)
128 | const [value, setValue] = useState('')
129 |
130 | useEffect(() => {
131 | if (error) alert('error')
132 | if (data === 'success') alert('success')
133 | }, [error, data])
134 |
135 | return (
136 |
137 | setValue(e.target.value)} />
138 | mutate(value)}>submit
139 |
140 | )
141 | }
142 | ```
143 |
144 | ## Api
145 |
146 | ```ts
147 | type useRequest = (api, initialState) => [state, memoizedRequestCallback]
148 | ```
149 |
150 | Notice: Why _momoized_ request callback ?
151 |
152 | Reference: [Is it safe to omit functions from the list of dependencies?](https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies)
153 |
154 | If you want a deep dive on useEffect and dependencies, it's here: https://overreacted.io/a-complete-guide-to-useeffect/
155 |
156 | ## Contribution
157 |
158 | PR & issue welcome.
159 |
160 | ## License
161 |
162 | MIT
163 |
--------------------------------------------------------------------------------
/example/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/example/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const instance = axios.create({
4 | baseURL: 'https://dog.ceo/api',
5 | timeout: 1000,
6 | })
7 |
8 | instance.interceptors.response.use(function (response) {
9 | return response.data.message
10 | }, function (error) {
11 | return Promise.reject(error)
12 | })
13 |
14 | interface Breeds {
15 | [propName: string]: string[];
16 | }
17 |
18 | export function listAllBreeds (): Promise {
19 | return instance.get('/breeds/list/all')
20 | }
21 |
22 | export function getRandomImagesOfBreeds (breed: string): string {
23 | return instance.get(`/breed/${breed}/images/random`)
24 | }
25 |
26 | interface BreedsList {
27 | key: string;
28 | value: string;
29 | }
30 |
31 | export function transform (data: Breeds): BreedsList[] {
32 | const result: BreedsList[] = []
33 |
34 | Object.entries(data).forEach(([key, value]) => {
35 | if (value.length > 0) {
36 | result.push(...value.map(item => ({
37 | key: key + '/' + item,
38 | value: item + ' ' + key,
39 | })))
40 | } else {
41 | result.push({
42 | key,
43 | value: key,
44 | })
45 | }
46 | })
47 |
48 | return result
49 | }
50 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import 'react-app-polyfill/ie11'
2 | import * as React from 'react'
3 | import * as ReactDOM from 'react-dom'
4 | import useRequest from '../src'
5 | import { listAllBreeds, getRandomImagesOfBreeds, transform } from './axios'
6 |
7 | const App: React.FC = () => {
8 | const [{ loading, error, data }, fetchList] = useRequest(listAllBreeds)
9 | const [imageState, fetchImage] = useRequest(getRandomImagesOfBreeds)
10 |
11 | function handleClick(breed: string): void {
12 | fetchImage(breed)
13 | }
14 |
15 | React.useEffect(() => {
16 | fetchList()
17 | }, [fetchList])
18 |
19 | const renderImage = () => {
20 | const { loading, error, data } = imageState
21 |
22 | if (loading) return loading...
23 | if (error) return error
24 |
25 | return data &&
26 | }
27 |
28 | if (loading) return loading...
29 | if (error) return error
30 |
31 | const list = transform(data || {})
32 |
33 | return (
34 |
35 |
36 | {list.map(item => (
37 | handleClick(item.key)}>
38 | {item.value}
39 |
40 | ))}
41 |
42 |
{renderImage()}
43 |
44 | )
45 | }
46 |
47 | ReactDOM.render( , document.getElementById('root'))
48 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "axios": "^0.19.0",
12 | "react-app-polyfill": "^1.0.0"
13 | },
14 | "alias": {
15 | "react": "../node_modules/react",
16 | "react-dom": "../node_modules/react-dom/profiling",
17 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^16.8.15",
21 | "@types/react-dom": "^16.8.4",
22 | "parcel": "^1.12.3",
23 | "typescript": "^3.4.5"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["es2015", "es2016", "dom"],
16 | "baseUrl": ".",
17 | "types": ["node"]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@rekindle/use-request",
3 | "version": "0.3.0",
4 | "main": "dist/index.js",
5 | "module": "dist/use-request.esm.js",
6 | "typings": "dist/index.d.ts",
7 | "files": [
8 | "dist"
9 | ],
10 | "scripts": {
11 | "start": "tsdx watch",
12 | "build": "tsdx build",
13 | "test": "tsdx test --env=jsdom",
14 | "lint": "tsdx lint"
15 | },
16 | "peerDependencies": {
17 | "react": ">=16.8"
18 | },
19 | "husky": {
20 | "hooks": {
21 | "pre-commit": "pretty-quick --staged"
22 | }
23 | },
24 | "prettier": {
25 | "printWidth": 80,
26 | "semi": false,
27 | "singleQuote": true,
28 | "trailingComma": "es5"
29 | },
30 | "devDependencies": {
31 | "@types/jest": "^24.0.17",
32 | "@types/react": "^16.9.2",
33 | "@types/react-dom": "^16.8.5",
34 | "husky": "^3.0.4",
35 | "prettier": "^1.18.2",
36 | "pretty-quick": "^1.11.1",
37 | "react": "^16.9.0",
38 | "react-dom": "^16.9.0",
39 | "tsdx": "^0.8.0",
40 | "tslib": "^1.10.0",
41 | "typescript": "^3.5.3"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { useReducer, useCallback } from 'react'
2 | import { ActionType, IState, IAction, Unpacked } from './types'
3 |
4 | /** define request status in three types */
5 | const REQUEST_INIT: ActionType = 'REQUEST_INIT'
6 | const REQUEST_SUCCESS: ActionType = 'REQUEST_SUCCESS'
7 | const REQUEST_FAILURE: ActionType = 'REQUEST_FAILURE'
8 |
9 | /** define default initial state */
10 | const defaultInitialState: IState = {
11 | loading: false,
12 | data: null,
13 | error: null,
14 | }
15 |
16 | /**
17 | *reducer function
18 | *
19 | * @template T
20 | * @param {T} state
21 | * @param {IAction} action
22 | * @returns {IState}
23 | */
24 | function reducer> (state: T,action: IAction): IState | never {
25 | switch (action.type) {
26 | case REQUEST_INIT:
27 | return { ...defaultInitialState, loading: true }
28 | case REQUEST_SUCCESS:
29 | return { ...state, loading: false, data: action.payload }
30 | case REQUEST_FAILURE:
31 | return { ...state, loading: false, error: action.error }
32 | default:
33 | throw new Error('error')
34 | }
35 | }
36 |
37 | /**
38 | * request function
39 | *
40 | * @template T
41 | * @param {T} instance
42 | * @param {(aciton: IAction>) => void} dispatch
43 | * @returns {Promise}
44 | */
45 | async function request any> (
46 | instance: T,
47 | dispatch: (aciton: IAction>) => void,
48 | ): Promise> {
49 | try {
50 | dispatch({ type: REQUEST_INIT })
51 | const result = await instance()
52 | dispatch({ type: REQUEST_SUCCESS, payload: result })
53 | return result
54 | } catch (error) {
55 | dispatch({ type: REQUEST_FAILURE, error })
56 | throw error
57 | }
58 | }
59 |
60 | /**
61 | * main function
62 | *
63 | * @template T
64 | * @param {T} instance
65 | * @param {IState>>} [initialState]
66 | * @returns
67 | */
68 | function useRequest any> (
69 | instance: T,
70 | initialState?: IState>>
71 | ) {
72 | const initialIState = {
73 | ...defaultInitialState,
74 | ...initialState,
75 | }
76 | const [state, dispatch] = useReducer(reducer, initialIState)
77 |
78 | function requestCallback (...args: Parameters) {
79 | return request((): Unpacked> => instance(...args), dispatch)
80 | }
81 |
82 | const memoizedRequestCallback = useCallback(requestCallback, [])
83 |
84 | return [state, memoizedRequestCallback] as const
85 | }
86 |
87 | export default useRequest
88 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 |
2 | /** data type for user */
3 | export type DataType = T | null
4 |
5 | /** TODO: add generics */
6 | /** error type */
7 | export type ErrorType = string | Error | null
8 |
9 | /** action types */
10 | export type ActionType =
11 | /** mark before request start status */
12 | | 'REQUEST_INIT'
13 | /** mark request success status */
14 | | 'REQUEST_SUCCESS'
15 | /** mark request failure status */
16 | | 'REQUEST_FAILURE'
17 |
18 | /** state */
19 | export interface IState {
20 | loading: boolean;
21 | data: DataType;
22 | error: ErrorType;
23 | }
24 |
25 | /** action */
26 | export interface IAction {
27 | type: ActionType;
28 | payload?: DataType;
29 | error?: ErrorType;
30 | }
31 |
32 | /** utility type for unpacking a type */
33 | export type Unpacked =
34 | T extends (infer U)[] ? U :
35 | T extends (...args: any[]) => infer U ? U :
36 | T extends Promise ? U :
37 | T
--------------------------------------------------------------------------------
/test/blah.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import { Thing } from '../src';
4 |
5 | describe('it', () => {
6 | it('renders without crashing', () => {
7 | const div = document.createElement('div');
8 | ReactDOM.render( , div);
9 | ReactDOM.unmountComponentAtNode(div);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types"],
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | "declaration": true,
9 | "sourceMap": true,
10 | "rootDir": "./",
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "strictFunctionTypes": true,
15 | "strictPropertyInitialization": true,
16 | "noImplicitThis": true,
17 | "alwaysStrict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noImplicitReturns": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "moduleResolution": "node",
23 | "baseUrl": "./",
24 | "paths": {
25 | "*": ["src/*", "node_modules/*"]
26 | },
27 | "jsx": "react",
28 | "esModuleInterop": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------