├── .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 | 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 | 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 | 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 | --------------------------------------------------------------------------------