├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── jest.config.js ├── package.json ├── src ├── __snapshots__ │ └── usePromise.spec.ts.snap ├── usePromise.spec.ts └── usePromise.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts2_cache_cjs 5 | .rts2_cache_es 6 | .rts2_cache_umd 7 | dist 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Jiri Spac. 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 | # react-promise 2 | 3 | [![NPM badge](https://nodei.co/npm/react-promise.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/react-promise/) 4 | 5 | a react.js hook for general promise written in typescript. 6 | Let's consider a trivial example: you have a promise such as this 7 | 8 | ```javascript 9 | let prom = new Promise(function(resolve, reject) { 10 | setTimeout(function() { 11 | resolve('a value') 12 | }, 100) 13 | }) 14 | ``` 15 | 16 | and you want to make a component, which renders out in it's body 'a value'. Without react-promise, such component may look like this: 17 | 18 | ```javascript 19 | class ExampleWithoutAsync extends React.Component { // you can't use stateless component because you need a state 20 | constructor () { 21 | super() 22 | this.state = {} 23 | } 24 | componentDidMount() { 25 | prom.then((value) => { 26 | this.setState({val: value}) 27 | }) 28 | } 29 | render () { 30 | if (!this.state.val) return null 31 | return
{this.state.val}
32 | } 33 | 34 | // or you could use a combination of useEffect and useState hook, which is basically the implementation of this small library 35 | ``` 36 | 37 | and with react-promise: 38 | 39 | ```tsx 40 | import usePromise from 'react-promise'; 41 | 42 | const ExampleWithAsync = (props) => { 43 | const {value, loading} = usePromise(prom) 44 | if (loading) return null 45 | return
{value}
} 46 | } 47 | ``` 48 | 49 | ## API 50 | 51 | The only argument can be a promise or a promise resolving thunk: 52 | 53 | ```ts 54 | usePromise( 55 | promiseOrFn: (() => Promise) | Promise 56 | ) 57 | ``` 58 | 59 | it might be desirable to let the usePromise call the promise returnig function, because you often don't want to do that inside the render of your functional component. 60 | 61 | Full state object interface returned by the hook looks like: 62 | 63 | ```ts 64 | { 65 | loading: boolean 66 | error: Error | null 67 | value: T | undefined // where T is your shape of the resolved data you expect obviously 68 | } 69 | ``` 70 | 71 | ## install 72 | 73 | With npm: 74 | 75 | ``` 76 | npm i react-promise 77 | ``` 78 | 79 | [For version 2 docs, refer to the old readme](https://github.com/capaj/react-promise/tree/1691b7202be806db5f41784d6e2cc9d231a3975c#react-promise). 80 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testRegex: '(\\.(spec))\\.(js?|ts?|jsx?|tsx?)$' 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-promise", 3 | "version": "3.0.2", 4 | "source": "src/usePromise.ts", 5 | "main": "dist/cjs/usePromise.js", 6 | "module": "dist/esm/usePromise.js", 7 | "typings": "dist/esm/usePromise.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build": "yarn build:esm && yarn build:cjs", 13 | "prepare": "npm run build", 14 | "build:esm": "tsc --module esnext --target esnext --outDir dist/esm", 15 | "build:cjs": "tsc --module commonjs --target es5 --outDir dist/cjs", 16 | "test": "jest", 17 | "jw": "jest --watch" 18 | }, 19 | "devDependencies": { 20 | "@types/jest": "^24.0.12", 21 | "@types/react": "^16.8.15", 22 | "husky": "^2.1.0", 23 | "jest": "^24.7.1", 24 | "microbundle": "^0.11.0", 25 | "prettier": "^1.17.0", 26 | "pretty-quick": "^1.10.0", 27 | "react": "^16.8.6", 28 | "react-hooks-testing-library": "^0.5.0", 29 | "react-test-renderer": "^16.8.6", 30 | "ts-jest": "^24.0.2", 31 | "typescript": "^3.4.5" 32 | }, 33 | "husky": { 34 | "hooks": { 35 | "pre-commit": "pretty-quick --staged" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/__snapshots__/usePromise.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`usePromise works for rejected promise 1`] = ` 4 | Object { 5 | "error": null, 6 | "loading": true, 7 | "value": undefined, 8 | } 9 | `; 10 | 11 | exports[`usePromise works for rejected promise 2`] = ` 12 | Object { 13 | "error": [Error: an error], 14 | "loading": false, 15 | "value": undefined, 16 | } 17 | `; 18 | 19 | exports[`usePromise works for resolved promise 1`] = ` 20 | Object { 21 | "error": null, 22 | "loading": true, 23 | "value": undefined, 24 | } 25 | `; 26 | 27 | exports[`usePromise works for resolved promise 2`] = ` 28 | Object { 29 | "error": null, 30 | "loading": false, 31 | "value": "a value", 32 | } 33 | `; 34 | 35 | exports[`usePromise works when providing a promise thunk 1`] = ` 36 | Object { 37 | "error": null, 38 | "loading": true, 39 | "value": undefined, 40 | } 41 | `; 42 | 43 | exports[`usePromise works when providing a promise thunk 2`] = ` 44 | Object { 45 | "error": null, 46 | "loading": false, 47 | "value": "a value from promise returning thunk", 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /src/usePromise.spec.ts: -------------------------------------------------------------------------------- 1 | import usePromise from './usePromise' 2 | import { renderHook, act } from 'react-hooks-testing-library' 3 | 4 | describe('usePromise', () => { 5 | it('works for resolved promise', (done) => { 6 | let prom = new Promise((resolve) => { 7 | setTimeout(function() { 8 | act(() => { 9 | resolve('a value') 10 | }) 11 | }, 49) 12 | }) 13 | 14 | renderHook(() => { 15 | act(() => { 16 | expect(usePromise(prom)).toMatchSnapshot() 17 | }) 18 | 19 | setTimeout(() => { 20 | done() 21 | }, 60) 22 | }) 23 | }) 24 | 25 | it('works for rejected promise', (done) => { 26 | let prom = new Promise((_, reject) => { 27 | setTimeout(function() { 28 | act(() => { 29 | reject(new Error('an error')) 30 | }) 31 | }, 49) 32 | }) 33 | renderHook(() => { 34 | act(() => { 35 | expect(usePromise(prom)).toMatchSnapshot() 36 | }) 37 | 38 | setTimeout(() => { 39 | done() 40 | }, 60) 41 | }) 42 | }) 43 | 44 | it('works when providing a promise thunk', (done) => { 45 | let promThunk = () => 46 | new Promise((resolve) => { 47 | setTimeout(function() { 48 | act(() => { 49 | resolve('a value from promise returning thunk') 50 | }) 51 | }, 49) 52 | }) 53 | 54 | renderHook(() => { 55 | act(() => { 56 | expect(usePromise(promThunk)).toMatchSnapshot() 57 | }) 58 | 59 | setTimeout(() => { 60 | done() 61 | }, 60) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/usePromise.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react' 2 | 3 | export default function usePromise( 4 | promiseOrFn: (() => Promise) | Promise 5 | ): { 6 | loading: boolean 7 | error: Error | null 8 | value: T | undefined 9 | } { 10 | const [state, setState] = useState<{ 11 | loading: boolean 12 | error: Error | null 13 | value: T | undefined 14 | }>({ 15 | loading: !!promiseOrFn, 16 | error: null, 17 | value: undefined 18 | }) 19 | const isMounted = useRef(false) 20 | useEffect(() => { 21 | isMounted.current = true 22 | if (!promiseOrFn) { 23 | setState({ 24 | loading: false, 25 | error: null, 26 | value: undefined 27 | }) 28 | } else { 29 | if (state.loading === false) { 30 | setState({ 31 | loading: true, 32 | error: null, 33 | value: undefined 34 | }) 35 | } 36 | let promise: Promise 37 | if (typeof promiseOrFn === 'function') { 38 | promise = promiseOrFn() 39 | } else { 40 | promise = promiseOrFn 41 | } 42 | 43 | promise 44 | .then((value) => { 45 | if (isMounted.current) { 46 | setState({ 47 | loading: false, 48 | error: null, 49 | value 50 | }) 51 | } 52 | }) 53 | .catch((error) => { 54 | if (isMounted.current) { 55 | setState({ 56 | loading: false, 57 | error, 58 | value: undefined 59 | }) 60 | } 61 | }) 62 | } 63 | 64 | return () => { 65 | isMounted.current = false 66 | } 67 | }, [promiseOrFn]) 68 | 69 | return state 70 | } 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | // "rootDir": "./", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "strictPropertyInitialization": true, 14 | "noImplicitThis": true, 15 | "alwaysStrict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "moduleResolution": "node", 21 | "outDir": "./dist", 22 | "baseUrl": "./", 23 | "paths": { 24 | "*": ["src/*", "node_modules/*"] 25 | }, 26 | "jsx": "react", 27 | "esModuleInterop": true 28 | }, 29 | "include": ["src", "types"], 30 | "exclude": ["**/*.spec.ts"] 31 | } 32 | --------------------------------------------------------------------------------