├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── main.yml │ └── publish.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── PromiseIdle.d.ts ├── PromiseIdle.js ├── PromiseLoading.d.ts ├── PromiseLoading.js ├── PromiseRejected.d.ts ├── PromiseRejected.js ├── PromiseResolved.d.ts ├── PromiseResolved.js ├── index-commonjs.cjs ├── index-commonjs.cjs.map ├── index.d.ts ├── index.js ├── index.mjs ├── index.mjs.map ├── types.d.ts ├── types.js ├── usePromiseMatcher.d.ts ├── usePromiseMatcher.js ├── usePromiseWithInterval.d.ts └── usePromiseWithInterval.js ├── jest.config.json ├── package.json ├── rollup.config.js ├── src ├── PromiseIdle.test.ts ├── PromiseIdle.ts ├── PromiseLoading.test.ts ├── PromiseLoading.ts ├── PromiseRejected.test.ts ├── PromiseRejected.ts ├── PromiseResolved.test.ts ├── PromiseResolved.ts ├── index.ts ├── types.ts ├── usePromiseMatcher.test.tsx ├── usePromiseMatcher.ts ├── usePromiseWithInterval.test.tsx └── usePromiseWithInterval.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react 4 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 6 | "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | "parser": "@typescript-eslint/parser", 9 | "plugins": ["@typescript-eslint/eslint-plugin", "prettier"], 10 | "settings": { 11 | "import/parsers": { 12 | "@typescript-eslint/parser": [".ts", ".tsx"] 13 | }, 14 | "import/resolver": { 15 | "typescript": {} 16 | }, 17 | "react": { 18 | "version": "detect" 19 | } 20 | }, 21 | "rules": { 22 | "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], 23 | "prettier/prettier": "error", 24 | "@typescript-eslint/no-explicit-any": "off", 25 | "@typescript-eslint/no-use-before-define": "error", 26 | "react/display-name": ["off"], 27 | "react/prop-types": ["off"], 28 | "@typescript-eslint/explicit-function-return-type": ["off"], 29 | "@typescript-eslint/no-unused-vars": [ 30 | "error", 31 | { 32 | "argsIgnorePattern": "^_", 33 | "varsIgnorePattern": "^_", 34 | "caughtErrorsIgnorePattern": "^_" 35 | } 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v2 26 | with: 27 | node-version: '14' 28 | - name: Install dependencies 29 | run: yarn install --frozen-lockfile 30 | - name: Lint 31 | run: yarn lint 32 | - name: Build 33 | run: yarn build 34 | - name: Test 35 | run: yarn test-ci 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '14' 16 | registry-url: 'https://registry.npmjs.org' 17 | - name: Install dependencies 18 | run: yarn install --frozen-lockfile 19 | - name: Build 20 | run: yarn build 21 | - name: Test 22 | run: yarn test-ci 23 | - name: Publish 24 | run: yarn publish 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # VSCODE 2 | .vscode 3 | 4 | # DEPENDENCIES 5 | node_modules 6 | 7 | # LOGGING 8 | 9 | *.log 10 | logs 11 | 12 | # OS 13 | .DS_* 14 | 15 | # WEBSTORM 16 | .idea 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "semi": true, 4 | "trailingComma": "all", 5 | "singleQuote": false, 6 | "printWidth": 120, 7 | "tabWidth": 4 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.5.0] 2 | 3 | - [Fix - [Issue #32](https://github.com/softwaremill/react-use-promise-matcher/issues/32)] Updated to React v18 and fixed related state management issues. 4 | - Fixed issues with Webpack 5 5 | 6 | ## [1.4.2] 7 | 8 | - Added manual retry option and tryCount to usePromiseWithInterval 9 | 10 | ## [1.4.1] 11 | 12 | - README.md update: contents list, more concise feature description 13 | 14 | ## [1.4.0] 15 | 16 | - Chainable callback methods added: `onResolved`, `onRejected`, `onIdle`, and `onLoading` 17 | - Added a `usePromiseWithInterval` hook for repetitive polling 18 | 19 | ## [1.3.2] 20 | 21 | - Added flatMap function to PromiseResultShape api 22 | - Migrated to GitHub Actions 23 | 24 | ## [1.3.1] 25 | 26 | - Updated documentation 27 | 28 | ## [1.3.0] 29 | 30 | - BREAKING CHANGE: Removed `usePromiseWithArguments` - the functionality of both is now contained in `usePromise`. 31 | - BREAKING CHANGE: Removed config object from `usePromise` arguments list (there was just `autoLoad` flag at this point) 32 | - Safe state handling within the hook - avoiding "Warning: Can't perform a React state update on an unmounted component." error in the situation where Promise resolves after the component was unmounted. 33 | - Upgraded @testing-library dependencies 34 | - Removed usages of deprecated `waitForElement` in tests 35 | 36 | ## [1.2.1] 37 | 38 | - Added helper type-guard functions for asserting PromiseRejected and PromiseResolved types in order to access the values directly. 39 | 40 | ## [1.2.0] 41 | 42 | - Changed the return value of the hooks from object to array. 43 | 44 | ## [1.1.0][corrupted] 45 | 46 | ## [1.0.8] 47 | 48 | - Wrapped loading functions in useCallback 49 | 50 | ## [1.0.7] 51 | 52 | - Corrected commonjs build configuration so that tests in Jest in projects importing the library won't fail 53 | 54 | ## [1.0.6] 55 | 56 | - Updated build configuration 57 | 58 | ## [1.0.5] 59 | 60 | - Updated documentation. 61 | 62 | ## [1.0.4] 63 | 64 | - Added CommonJS output file. 65 | 66 | ## [1.0.3] 67 | 68 | - Updated documentation. 69 | 70 | ## [1.0.2] 71 | 72 | - Deploy to npm 73 | 74 | ## [1.0.1] 75 | 76 | - [corrupted] Deployed to npm in previous removed publishment 77 | 78 | ## [1.0.0] 79 | 80 | - [corrupted] Deployed to npm in previous removed publishment 81 | 82 | ## [0.2.2] 83 | 84 | - Simplified rollup config 85 | - Updated rollup config - removed building umd module 86 | 87 | ## [0.2.1] 88 | 89 | - Project configuration updates - migrated from webpack to rollup 90 | 91 | ## [0.2.0] 92 | 93 | - Fixed bug with hook entering a re-render loop when config was passed to usePromise 94 | - Removed 'P extends object' type constraint for loader function argument type in usePromiseWithArguments 95 | - Added a "clear" funtion to the object returned by the hooks. 96 | - Updated README 97 | 98 | ## [0.1.0] 99 | 100 | - Initial version 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SoftwareMill 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-use-promise-matcher 2 | 3 | ## Contents 4 | 5 | - [Installation](#installation) 6 | - [npm](#npm) 7 | - [yarn](#yarn) 8 | - [About](#about) 9 | - [Features](#features) 10 | - [Hooks](#hooks) 11 | - [usePromise](#usepromise) 12 | - [basic usage](#basic-usage) 13 | - [with arguments](#with-arguments) 14 | - [error handling](#error-handling) 15 | - [Polling: usePromiseWithInterval](#polling-usepromisewithinterval) 16 | - [Callback functions](#callback-functions) 17 | 18 | ## Installation 19 | 20 | #### npm 21 | 22 | ```bash 23 | npm install react-use-promise-matcher 24 | ``` 25 | 26 | #### yarn 27 | 28 | ```bash 29 | yarn add react-use-promise-matcher 30 | ``` 31 | 32 | ## About 33 | 34 | This library provides two hooks that aim to facilitate working with asynchronous data in React. Implementing components that depend on some data fetched from an API can generate a significant amount of boilerplate code as it is a common practice to handle the following situations: 35 | 36 | 1. The data hasn't been loaded yet, so we want to display some kind of loading feedback to the user. 37 | 2. Request failed with an error and we should handle this situation somehow. 38 | 3. Request was successful and we want to render the component. 39 | 40 | Unfortunately, we cannot monitor the state of a `Promise` as it is a stateless object. We can keep the information about the status of the request within the component's state though, however this usually forces us to repeat the same code across many components. 41 | 42 | The `usePromise` and `usePromiseWithArguments` hooks let you manage the state of a `Promise` without redundant boilerplate by using the `PromiseResultShape` object, which is represented by four states: 43 | 44 | - `Idle` - a request hasn't been sent yet 45 | - `Loading` - waiting for the `Promise` to resolve 46 | - `Rejected` - the `Promise` was rejected 47 | - `Resolved` - the `Promise` was resolved successfully 48 | 49 | The `PromiseResultShape` provides an API that lets you match each of the states and perform some actions accordingly or map them to some value, which is the main use case, as we would normally map the states to different components. Let's have a look at some examples then ... 50 | 51 | ## Features 52 | 53 | ### Hooks 54 | 55 | #### usePromise 56 | 57 | ##### Basic usage 58 | 59 | Let's assume we have a simple echo method that returns the string provided as an argument wrapped in a Promise. 60 | This is how we would use the `usePromise` hook to render the received text based on what the method returns: 61 | 62 | ```tsx 63 | const echo = (text: string): Promise => new Promise((resolve) => setTimeout(() => resolve(text), 3000)); 64 | 65 | export const EchoComponent = () => { 66 | const [result, load] = usePromise(() => echo("Echo!")); 67 | 68 | React.useEffect(() => { 69 | load(); 70 | }, []); 71 | 72 | return result.match({ 73 | Idle: () => <>, 74 | Loading: () => I say "echo!", 75 | Rejected: (err) => Oops, something went wrong! Error: {err}, 76 | Resolved: (echoResponse) => Echo says "{echoResponse}", 77 | }); 78 | }; 79 | ``` 80 | 81 | The hook accepts a function that returns a `Promise`, as simple as that. The type parameter defines the type of data wrapped by the `Promise`. It returns an array with a `result` object that represents the result of the asynchronous operation and lets us match its states and a `load` function which simply calls the function provided as an argument within the hook. 82 | 83 | It's also worth mentioning that matching the `Idle` state is optional - the mapping for the `Loading` state will be taken for the `Idle` state if none is passed. 84 | 85 | ##### With arguments 86 | 87 | Sometimes it is necessary to pass some arguments to the promise loading function. You can simply pass such function as an argument to he `usePromise` hook, it's type safe as the types of the arguments will be inferred in the returned loading function. It's also possible to explicitly define them in the second type argument of the hook. 88 | 89 | ```tsx 90 | export const UserEchoComponent = () => { 91 | const [text, setText] = React.useState("Hello!"); 92 | const echoWithArguments = (param: string) => echo(param); 93 | const [result, load] = usePromise(echoWithArguments); 94 | const onInputChange = (e: React.ChangeEvent) => setText(e.target.value); 95 | 96 | const callEcho = React.useCallback(() => { 97 | () => load(text); 98 | }, [load]); 99 | 100 | return ( 101 |
102 | 103 | 104 | {result.match({ 105 | Idle: () => <>, 106 | Loading: () => I say "{text}"!, 107 | Rejected: (err) => Oops, something went wrong! Error: {err}, 108 | Resolved: (echoResponse) => Echo says "{echoResponse}", 109 | })} 110 |
111 | ); 112 | }; 113 | ``` 114 | 115 | ##### Error handling 116 | 117 | We can provide a third type parameter to the hook, which defines the type of error that is returned on rejection. By default, it is set to string. If we are using some type of domain exceptions in our services we could use the hook as following: 118 | 119 | ```tsx 120 | const [result] = usePromise(() => myServiceMethod(someArgument)); 121 | ``` 122 | 123 | and then we would match the `Rejected` state like that: 124 | 125 | ```typescript 126 | result.match({ 127 | //... 128 | Rejected: (err: MyDomainException) => err.someCustomField, 129 | //... 130 | }); 131 | ``` 132 | 133 | #### Polling: usePromiseWithInterval 134 | 135 | If you need to repeatedly poll the data (eg. by sending a request to the server), and do that on-demand, you can use `usePromiseWithInterval` hook. Pass the interval as a second argument, and receive the `result` and `start` & `stop` functions in return. `usePromiseInterval` uses `setTimeout` API for polling. 136 | 137 | ```tsx 138 | export const UserEchoWithIntervalComponent = () => { 139 | const echoWithArguments = (param: string) => echo(param); 140 | const [result, start, stop] = usePromiseWithInterval(echoWithArguments, 2000); 141 | 142 | const startCallingEcho = React.useCallback(() => { 143 | start("It's me again!!!"); 144 | }, [start]); 145 | 146 | return ( 147 | <> 148 | {result.match({ 149 | Idle: () => <>, 150 | Loading: () => I say "{text}"!, 151 | Rejected: (err) => Oops, something went wrong! Error: {err}, 152 | Resolved: (echoResponse) => Echo says "{echoResponse}", 153 | })} 154 | 155 | ); 156 | }; 157 | ``` 158 | 159 | Besides that, you can also perform calls manually, and check the current amount of times your request was performed. 160 | 161 | ```tsx 162 | export const IntervalAndManualCheckComponent = () => { 163 | const echoWithArguments = (param: string) => echo(param); 164 | const [ 165 | result, // good old PromiseResultShape 166 | start, 167 | stop, 168 | load, // manual load trigger 169 | reset, // promise shape reset function 170 | tryCount // amount of times your request was performed 171 | ] = usePromiseWithInterval(echoWithArguments, 2000); 172 | 173 | const startCallingEcho = React.useCallback(() => { 174 | start("It's me again!!!"); 175 | }, [start]); 176 | 177 | return ( 178 | <> 179 | {result.match({ 180 | Idle: () => <>, 181 | Loading: () => I say "{text}"!, 182 | Rejected: (err) => Oops, something went wrong! Error: {err}, 183 | Resolved: (echoResponse) => Echo says "{echoResponse}", 184 | })} 185 | 186 | 187 | 188 | ); 189 | }; 190 | ``` 191 | 192 | ### Callback functions 193 | 194 | Apart from rendering phase, you may want to perform some side effect functions when your promise is in a specific state. I.e. you may want to invoke another asynchronous function when the data is resolved or when the error is being thrown. 195 | 196 | To do that, you can use callback functions dedicated to every promise state. 197 | 198 | ```tsx 199 | const [result1, load1] = usePromise(() => myServiceMethod(someArgument)); 200 | const [result2, load2] = usePromise(anotherServiceMethod); 201 | 202 | result1 203 | .onIdle(() => console.log("Promise is idle")) 204 | .onLoading(() => console.log("Yaaay, bring the data on!")) 205 | .onResolved((response) => load2(response.data)) 206 | .onRejected((err) => console.log(err.response.data)); 207 | 208 | React.useEffect(() => { 209 | // run this after the component mounts 210 | load1(); 211 | }, [load1]); 212 | ``` 213 | 214 | Every callback function is chainable - `onIdle`, `onLoading`, `onResolved` and `onRejected` return the `PromiseResultShape` instance. 215 | -------------------------------------------------------------------------------- /dist/PromiseIdle.d.ts: -------------------------------------------------------------------------------- 1 | import { PromiseMatcher, PromiseResultShape } from "./types"; 2 | export declare class PromiseIdle implements PromiseResultShape { 3 | isIdle: boolean; 4 | isLoading: boolean; 5 | isResolved: boolean; 6 | isRejected: boolean; 7 | match: (matcher: PromiseMatcher) => U; 8 | map: () => PromiseResultShape; 9 | flatMap: () => PromiseResultShape; 10 | mapErr: () => PromiseResultShape; 11 | get: () => T; 12 | getOr: (orValue: T) => T; 13 | onResolved: (_: (value: T) => unknown) => this; 14 | onRejected: (_: (err: E) => unknown) => this; 15 | onLoading: (_: () => unknown) => this; 16 | onIdle: (fn: () => unknown) => this; 17 | } 18 | -------------------------------------------------------------------------------- /dist/PromiseIdle.js: -------------------------------------------------------------------------------- 1 | export class PromiseIdle { 2 | constructor() { 3 | this.isIdle = true; 4 | this.isLoading = false; 5 | this.isResolved = false; 6 | this.isRejected = false; 7 | this.match = (matcher) => (matcher.Idle ? matcher.Idle() : matcher.Loading()); 8 | this.map = () => new PromiseIdle(); 9 | this.flatMap = () => new PromiseIdle(); 10 | this.mapErr = () => new PromiseIdle(); 11 | this.get = () => { 12 | throw new Error("Cannot get the value while the Promise is idle"); 13 | }; 14 | this.getOr = (orValue) => orValue; 15 | this.onResolved = (_) => { 16 | return this; 17 | }; 18 | this.onRejected = (_) => { 19 | return this; 20 | }; 21 | this.onLoading = (_) => { 22 | return this; 23 | }; 24 | this.onIdle = (fn) => { 25 | fn(); 26 | return this; 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /dist/PromiseLoading.d.ts: -------------------------------------------------------------------------------- 1 | import { PromiseMatcher, PromiseResultShape } from "./types"; 2 | export declare class PromiseLoading implements PromiseResultShape { 3 | isIdle: boolean; 4 | isLoading: boolean; 5 | isResolved: boolean; 6 | isRejected: boolean; 7 | match: (matcher: PromiseMatcher) => U; 8 | map: () => PromiseResultShape; 9 | flatMap: () => PromiseResultShape; 10 | mapErr: () => PromiseResultShape; 11 | get: () => T; 12 | getOr: (orValue: T) => T; 13 | onResolved: (_: (value: T) => unknown) => this; 14 | onRejected: (_: (err: E) => unknown) => this; 15 | onLoading: (fn: () => unknown) => this; 16 | onIdle: (_: () => unknown) => this; 17 | } 18 | -------------------------------------------------------------------------------- /dist/PromiseLoading.js: -------------------------------------------------------------------------------- 1 | export class PromiseLoading { 2 | constructor() { 3 | this.isIdle = false; 4 | this.isLoading = true; 5 | this.isResolved = false; 6 | this.isRejected = false; 7 | this.match = (matcher) => matcher.Loading(); 8 | this.map = () => new PromiseLoading(); 9 | this.flatMap = () => new PromiseLoading(); 10 | this.mapErr = () => new PromiseLoading(); 11 | this.get = () => { 12 | throw new Error("Cannot get the value while the Promise is loading"); 13 | }; 14 | this.getOr = (orValue) => orValue; 15 | this.onResolved = (_) => { 16 | return this; 17 | }; 18 | this.onRejected = (_) => { 19 | return this; 20 | }; 21 | this.onLoading = (fn) => { 22 | fn(); 23 | return this; 24 | }; 25 | this.onIdle = (_) => { 26 | return this; 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /dist/PromiseRejected.d.ts: -------------------------------------------------------------------------------- 1 | import { PromiseMatcher, PromiseResultShape } from "./types"; 2 | export declare class PromiseRejected implements PromiseResultShape { 3 | reason: E; 4 | isIdle: boolean; 5 | isLoading: boolean; 6 | isResolved: boolean; 7 | isRejected: boolean; 8 | constructor(reason: E); 9 | match: (matcher: PromiseMatcher) => U; 10 | map: () => PromiseResultShape; 11 | flatMap: () => PromiseResultShape; 12 | mapErr: (fn: (err: E) => U) => PromiseResultShape; 13 | get: () => T; 14 | getOr: (orValue: T) => T; 15 | onResolved: (_: (value: T) => unknown) => this; 16 | onRejected: (fn: (err: E) => unknown) => this; 17 | onLoading: (_: () => unknown) => this; 18 | onIdle: (_: () => unknown) => this; 19 | } 20 | export declare const isPromiseRejected: (promiseResultShape: PromiseResultShape) => promiseResultShape is PromiseRejected; 21 | -------------------------------------------------------------------------------- /dist/PromiseRejected.js: -------------------------------------------------------------------------------- 1 | export class PromiseRejected { 2 | constructor(reason) { 3 | this.reason = reason; 4 | this.isIdle = false; 5 | this.isLoading = false; 6 | this.isResolved = false; 7 | this.isRejected = true; 8 | this.match = (matcher) => matcher.Rejected(this.reason); 9 | this.map = () => new PromiseRejected(this.reason); 10 | this.flatMap = () => new PromiseRejected(this.reason); 11 | this.mapErr = (fn) => new PromiseRejected(fn(this.reason)); 12 | this.get = () => { 13 | throw this.reason; 14 | }; 15 | this.getOr = (orValue) => orValue; 16 | this.onResolved = (_) => { 17 | return this; 18 | }; 19 | this.onRejected = (fn) => { 20 | fn(this.reason); 21 | return this; 22 | }; 23 | this.onLoading = (_) => { 24 | return this; 25 | }; 26 | this.onIdle = (_) => { 27 | return this; 28 | }; 29 | } 30 | } 31 | export const isPromiseRejected = (promiseResultShape) => promiseResultShape.isRejected; 32 | -------------------------------------------------------------------------------- /dist/PromiseResolved.d.ts: -------------------------------------------------------------------------------- 1 | import { PromiseMatcher, PromiseResultShape } from "./types"; 2 | export declare class PromiseResolved implements PromiseResultShape { 3 | value: T; 4 | isIdle: boolean; 5 | isLoading: boolean; 6 | isResolved: boolean; 7 | isRejected: boolean; 8 | constructor(value: T); 9 | match: (matcher: PromiseMatcher) => U; 10 | map: (fn: (value: T) => U) => PromiseResultShape; 11 | flatMap: (fn: (value: T) => PromiseResultShape) => PromiseResultShape; 12 | mapErr: () => PromiseResultShape; 13 | get: () => T; 14 | getOr: () => T; 15 | onResolved: (fn: (value: T) => unknown) => this; 16 | onRejected: (_: (err: E) => unknown) => this; 17 | onLoading: (_: () => unknown) => this; 18 | onIdle: (_: () => unknown) => this; 19 | } 20 | export declare const isPromiseResolved: (promiseResultShape: PromiseResultShape) => promiseResultShape is PromiseResolved; 21 | -------------------------------------------------------------------------------- /dist/PromiseResolved.js: -------------------------------------------------------------------------------- 1 | export class PromiseResolved { 2 | constructor(value) { 3 | this.value = value; 4 | this.isIdle = false; 5 | this.isLoading = false; 6 | this.isResolved = true; 7 | this.isRejected = false; 8 | this.match = (matcher) => matcher.Resolved(this.value); 9 | this.map = (fn) => new PromiseResolved(fn(this.value)); 10 | this.flatMap = (fn) => fn(this.value); 11 | this.mapErr = () => new PromiseResolved(this.value); 12 | this.get = () => { 13 | return this.value; 14 | }; 15 | this.getOr = () => this.get(); 16 | this.onResolved = (fn) => { 17 | fn(this.get()); 18 | return this; 19 | }; 20 | this.onRejected = (_) => { 21 | return this; 22 | }; 23 | this.onLoading = (_) => { 24 | return this; 25 | }; 26 | this.onIdle = (_) => { 27 | return this; 28 | }; 29 | } 30 | } 31 | export const isPromiseResolved = (promiseResultShape) => promiseResultShape.isResolved; 32 | -------------------------------------------------------------------------------- /dist/index-commonjs.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | var React = require('react'); 6 | var reactDom = require('react-dom'); 7 | 8 | /*! ***************************************************************************** 9 | Copyright (c) Microsoft Corporation. All rights reserved. 10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 11 | this file except in compliance with the License. You may obtain a copy of the 12 | License at http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 16 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 17 | MERCHANTABLITY OR NON-INFRINGEMENT. 18 | 19 | See the Apache Version 2.0 License for specific language governing permissions 20 | and limitations under the License. 21 | ***************************************************************************** */ 22 | 23 | function __awaiter(thisArg, _arguments, P, generator) { 24 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 25 | return new (P || (P = Promise))(function (resolve, reject) { 26 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 27 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 28 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 29 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 30 | }); 31 | } 32 | 33 | class PromiseLoading { 34 | constructor() { 35 | this.isIdle = false; 36 | this.isLoading = true; 37 | this.isResolved = false; 38 | this.isRejected = false; 39 | this.match = (matcher) => matcher.Loading(); 40 | this.map = () => new PromiseLoading(); 41 | this.flatMap = () => new PromiseLoading(); 42 | this.mapErr = () => new PromiseLoading(); 43 | this.get = () => { 44 | throw new Error("Cannot get the value while the Promise is loading"); 45 | }; 46 | this.getOr = (orValue) => orValue; 47 | this.onResolved = (_) => { 48 | return this; 49 | }; 50 | this.onRejected = (_) => { 51 | return this; 52 | }; 53 | this.onLoading = (fn) => { 54 | fn(); 55 | return this; 56 | }; 57 | this.onIdle = (_) => { 58 | return this; 59 | }; 60 | } 61 | } 62 | 63 | class PromiseRejected { 64 | constructor(reason) { 65 | this.reason = reason; 66 | this.isIdle = false; 67 | this.isLoading = false; 68 | this.isResolved = false; 69 | this.isRejected = true; 70 | this.match = (matcher) => matcher.Rejected(this.reason); 71 | this.map = () => new PromiseRejected(this.reason); 72 | this.flatMap = () => new PromiseRejected(this.reason); 73 | this.mapErr = (fn) => new PromiseRejected(fn(this.reason)); 74 | this.get = () => { 75 | throw this.reason; 76 | }; 77 | this.getOr = (orValue) => orValue; 78 | this.onResolved = (_) => { 79 | return this; 80 | }; 81 | this.onRejected = (fn) => { 82 | fn(this.reason); 83 | return this; 84 | }; 85 | this.onLoading = (_) => { 86 | return this; 87 | }; 88 | this.onIdle = (_) => { 89 | return this; 90 | }; 91 | } 92 | } 93 | const isPromiseRejected = (promiseResultShape) => promiseResultShape.isRejected; 94 | 95 | class PromiseResolved { 96 | constructor(value) { 97 | this.value = value; 98 | this.isIdle = false; 99 | this.isLoading = false; 100 | this.isResolved = true; 101 | this.isRejected = false; 102 | this.match = (matcher) => matcher.Resolved(this.value); 103 | this.map = (fn) => new PromiseResolved(fn(this.value)); 104 | this.flatMap = (fn) => fn(this.value); 105 | this.mapErr = () => new PromiseResolved(this.value); 106 | this.get = () => { 107 | return this.value; 108 | }; 109 | this.getOr = () => this.get(); 110 | this.onResolved = (fn) => { 111 | fn(this.get()); 112 | return this; 113 | }; 114 | this.onRejected = (_) => { 115 | return this; 116 | }; 117 | this.onLoading = (_) => { 118 | return this; 119 | }; 120 | this.onIdle = (_) => { 121 | return this; 122 | }; 123 | } 124 | } 125 | const isPromiseResolved = (promiseResultShape) => promiseResultShape.isResolved; 126 | 127 | class PromiseIdle { 128 | constructor() { 129 | this.isIdle = true; 130 | this.isLoading = false; 131 | this.isResolved = false; 132 | this.isRejected = false; 133 | this.match = (matcher) => (matcher.Idle ? matcher.Idle() : matcher.Loading()); 134 | this.map = () => new PromiseIdle(); 135 | this.flatMap = () => new PromiseIdle(); 136 | this.mapErr = () => new PromiseIdle(); 137 | this.get = () => { 138 | throw new Error("Cannot get the value while the Promise is idle"); 139 | }; 140 | this.getOr = (orValue) => orValue; 141 | this.onResolved = (_) => { 142 | return this; 143 | }; 144 | this.onRejected = (_) => { 145 | return this; 146 | }; 147 | this.onLoading = (_) => { 148 | return this; 149 | }; 150 | this.onIdle = (fn) => { 151 | fn(); 152 | return this; 153 | }; 154 | } 155 | } 156 | 157 | const usePromise = (loaderFn) => { 158 | const [result, setResult] = React.useState(new PromiseIdle()); 159 | const load = React.useCallback((...args) => __awaiter(void 0, void 0, void 0, function* () { 160 | setResult(new PromiseLoading()); 161 | try { 162 | const data = yield loaderFn(...args); 163 | reactDom.flushSync(() => setResult(new PromiseResolved(data))); 164 | } 165 | catch (err) { 166 | reactDom.flushSync(() => setResult(new PromiseRejected(err))); 167 | } 168 | }), [loaderFn]); 169 | const clear = () => setResult(new PromiseIdle()); 170 | return [result, load, clear]; 171 | }; 172 | 173 | const usePromiseWithInterval = (loaderFn, interval) => { 174 | const [result, load, reset] = usePromise(loaderFn); 175 | const [tryCount, setTryCount] = React.useState(0); 176 | const increment = React.useCallback(() => { 177 | setTryCount((v) => v + 1); 178 | }, [setTryCount]); 179 | const timer = React.useRef(undefined); 180 | const start = React.useCallback((...args) => { 181 | timer.current = setTimeout(function tick() { 182 | return __awaiter(this, void 0, void 0, function* () { 183 | yield load(...args); 184 | timer.current = setTimeout(tick, interval); 185 | }); 186 | }, interval); 187 | }, [load, interval, timer]); 188 | React.useEffect(() => { 189 | if (result.isLoading) { 190 | increment(); 191 | } 192 | }, [result, increment]); 193 | const stop = React.useCallback(() => { 194 | clearTimeout(timer.current); 195 | }, [timer]); 196 | React.useEffect(() => { 197 | return () => { 198 | clearTimeout(timer.current); 199 | timer.current = undefined; 200 | }; 201 | }, [timer]); 202 | return [result, start, stop, load, reset, tryCount]; 203 | }; 204 | 205 | exports.PromiseIdle = PromiseIdle; 206 | exports.PromiseLoading = PromiseLoading; 207 | exports.PromiseRejected = PromiseRejected; 208 | exports.PromiseResolved = PromiseResolved; 209 | exports.isPromiseRejected = isPromiseRejected; 210 | exports.isPromiseResolved = isPromiseResolved; 211 | exports.usePromise = usePromise; 212 | exports.usePromiseWithInterval = usePromiseWithInterval; 213 | //# sourceMappingURL=index-commonjs.cjs.map 214 | -------------------------------------------------------------------------------- /dist/index-commonjs.cjs.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index-commonjs.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"} -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { usePromise } from "./usePromiseMatcher"; 2 | export { usePromiseWithInterval } from "./usePromiseWithInterval"; 3 | export { PromiseIdle } from "./PromiseIdle"; 4 | export { PromiseLoading } from "./PromiseLoading"; 5 | export { PromiseRejected, isPromiseRejected } from "./PromiseRejected"; 6 | export { PromiseResolved, isPromiseResolved } from "./PromiseResolved"; 7 | export { PromiseMatcher, UsePromise, PromiseResultShape, PromiseLoader, UsePromiseWithInterval } from "./types"; 8 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | export { usePromise } from "./usePromiseMatcher"; 2 | export { usePromiseWithInterval } from "./usePromiseWithInterval"; 3 | export { PromiseIdle } from "./PromiseIdle"; 4 | export { PromiseLoading } from "./PromiseLoading"; 5 | export { PromiseRejected, isPromiseRejected } from "./PromiseRejected"; 6 | export { PromiseResolved, isPromiseResolved } from "./PromiseResolved"; 7 | -------------------------------------------------------------------------------- /dist/index.mjs: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useRef, useEffect } from 'react'; 2 | import { flushSync } from 'react-dom'; 3 | 4 | /*! ***************************************************************************** 5 | Copyright (c) Microsoft Corporation. All rights reserved. 6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 7 | this file except in compliance with the License. You may obtain a copy of the 8 | License at http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 12 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 13 | MERCHANTABLITY OR NON-INFRINGEMENT. 14 | 15 | See the Apache Version 2.0 License for specific language governing permissions 16 | and limitations under the License. 17 | ***************************************************************************** */ 18 | 19 | function __awaiter(thisArg, _arguments, P, generator) { 20 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 21 | return new (P || (P = Promise))(function (resolve, reject) { 22 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 23 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 24 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 25 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 26 | }); 27 | } 28 | 29 | class PromiseLoading { 30 | constructor() { 31 | this.isIdle = false; 32 | this.isLoading = true; 33 | this.isResolved = false; 34 | this.isRejected = false; 35 | this.match = (matcher) => matcher.Loading(); 36 | this.map = () => new PromiseLoading(); 37 | this.flatMap = () => new PromiseLoading(); 38 | this.mapErr = () => new PromiseLoading(); 39 | this.get = () => { 40 | throw new Error("Cannot get the value while the Promise is loading"); 41 | }; 42 | this.getOr = (orValue) => orValue; 43 | this.onResolved = (_) => { 44 | return this; 45 | }; 46 | this.onRejected = (_) => { 47 | return this; 48 | }; 49 | this.onLoading = (fn) => { 50 | fn(); 51 | return this; 52 | }; 53 | this.onIdle = (_) => { 54 | return this; 55 | }; 56 | } 57 | } 58 | 59 | class PromiseRejected { 60 | constructor(reason) { 61 | this.reason = reason; 62 | this.isIdle = false; 63 | this.isLoading = false; 64 | this.isResolved = false; 65 | this.isRejected = true; 66 | this.match = (matcher) => matcher.Rejected(this.reason); 67 | this.map = () => new PromiseRejected(this.reason); 68 | this.flatMap = () => new PromiseRejected(this.reason); 69 | this.mapErr = (fn) => new PromiseRejected(fn(this.reason)); 70 | this.get = () => { 71 | throw this.reason; 72 | }; 73 | this.getOr = (orValue) => orValue; 74 | this.onResolved = (_) => { 75 | return this; 76 | }; 77 | this.onRejected = (fn) => { 78 | fn(this.reason); 79 | return this; 80 | }; 81 | this.onLoading = (_) => { 82 | return this; 83 | }; 84 | this.onIdle = (_) => { 85 | return this; 86 | }; 87 | } 88 | } 89 | const isPromiseRejected = (promiseResultShape) => promiseResultShape.isRejected; 90 | 91 | class PromiseResolved { 92 | constructor(value) { 93 | this.value = value; 94 | this.isIdle = false; 95 | this.isLoading = false; 96 | this.isResolved = true; 97 | this.isRejected = false; 98 | this.match = (matcher) => matcher.Resolved(this.value); 99 | this.map = (fn) => new PromiseResolved(fn(this.value)); 100 | this.flatMap = (fn) => fn(this.value); 101 | this.mapErr = () => new PromiseResolved(this.value); 102 | this.get = () => { 103 | return this.value; 104 | }; 105 | this.getOr = () => this.get(); 106 | this.onResolved = (fn) => { 107 | fn(this.get()); 108 | return this; 109 | }; 110 | this.onRejected = (_) => { 111 | return this; 112 | }; 113 | this.onLoading = (_) => { 114 | return this; 115 | }; 116 | this.onIdle = (_) => { 117 | return this; 118 | }; 119 | } 120 | } 121 | const isPromiseResolved = (promiseResultShape) => promiseResultShape.isResolved; 122 | 123 | class PromiseIdle { 124 | constructor() { 125 | this.isIdle = true; 126 | this.isLoading = false; 127 | this.isResolved = false; 128 | this.isRejected = false; 129 | this.match = (matcher) => (matcher.Idle ? matcher.Idle() : matcher.Loading()); 130 | this.map = () => new PromiseIdle(); 131 | this.flatMap = () => new PromiseIdle(); 132 | this.mapErr = () => new PromiseIdle(); 133 | this.get = () => { 134 | throw new Error("Cannot get the value while the Promise is idle"); 135 | }; 136 | this.getOr = (orValue) => orValue; 137 | this.onResolved = (_) => { 138 | return this; 139 | }; 140 | this.onRejected = (_) => { 141 | return this; 142 | }; 143 | this.onLoading = (_) => { 144 | return this; 145 | }; 146 | this.onIdle = (fn) => { 147 | fn(); 148 | return this; 149 | }; 150 | } 151 | } 152 | 153 | const usePromise = (loaderFn) => { 154 | const [result, setResult] = useState(new PromiseIdle()); 155 | const load = useCallback((...args) => __awaiter(void 0, void 0, void 0, function* () { 156 | setResult(new PromiseLoading()); 157 | try { 158 | const data = yield loaderFn(...args); 159 | flushSync(() => setResult(new PromiseResolved(data))); 160 | } 161 | catch (err) { 162 | flushSync(() => setResult(new PromiseRejected(err))); 163 | } 164 | }), [loaderFn]); 165 | const clear = () => setResult(new PromiseIdle()); 166 | return [result, load, clear]; 167 | }; 168 | 169 | const usePromiseWithInterval = (loaderFn, interval) => { 170 | const [result, load, reset] = usePromise(loaderFn); 171 | const [tryCount, setTryCount] = useState(0); 172 | const increment = useCallback(() => { 173 | setTryCount((v) => v + 1); 174 | }, [setTryCount]); 175 | const timer = useRef(undefined); 176 | const start = useCallback((...args) => { 177 | timer.current = setTimeout(function tick() { 178 | return __awaiter(this, void 0, void 0, function* () { 179 | yield load(...args); 180 | timer.current = setTimeout(tick, interval); 181 | }); 182 | }, interval); 183 | }, [load, interval, timer]); 184 | useEffect(() => { 185 | if (result.isLoading) { 186 | increment(); 187 | } 188 | }, [result, increment]); 189 | const stop = useCallback(() => { 190 | clearTimeout(timer.current); 191 | }, [timer]); 192 | useEffect(() => { 193 | return () => { 194 | clearTimeout(timer.current); 195 | timer.current = undefined; 196 | }; 197 | }, [timer]); 198 | return [result, start, stop, load, reset, tryCount]; 199 | }; 200 | 201 | export { PromiseIdle, PromiseLoading, PromiseRejected, PromiseResolved, isPromiseRejected, isPromiseResolved, usePromise, usePromiseWithInterval }; 202 | //# sourceMappingURL=index.mjs.map 203 | -------------------------------------------------------------------------------- /dist/index.mjs.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"} -------------------------------------------------------------------------------- /dist/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface PromiseMatcher { 2 | Resolved: (value: T) => U; 3 | Rejected: (reason: E) => U; 4 | Loading: () => U; 5 | Idle?: () => U; 6 | } 7 | export interface PromiseResultShape { 8 | match: (matcher: PromiseMatcher) => U; 9 | map: (fn: (value: T) => U) => PromiseResultShape; 10 | flatMap: (fn: (value: T) => PromiseResultShape) => PromiseResultShape; 11 | mapErr: (fn: (err: E) => U) => PromiseResultShape; 12 | get: () => T; 13 | getOr: (orValue: T) => T; 14 | onResolved: (fn: (value: T) => unknown) => PromiseResultShape; 15 | onRejected: (fn: (err: E) => unknown) => PromiseResultShape; 16 | onLoading: (fn: () => unknown) => PromiseResultShape; 17 | onIdle: (fn: () => unknown) => PromiseResultShape; 18 | isIdle: boolean; 19 | isLoading: boolean; 20 | isResolved: boolean; 21 | isRejected: boolean; 22 | } 23 | export declare type PromiseLoader = (...args: Args) => Promise; 24 | export declare type UsePromise = [ 25 | result: PromiseResultShape, 26 | load: PromiseLoader, 27 | reset: () => void 28 | ]; 29 | export declare type UsePromiseWithInterval = [ 30 | result: PromiseResultShape, 31 | start: (...args: A) => void, 32 | stop: () => void, 33 | load: PromiseLoader, 34 | reset: () => void, 35 | tryCount: number 36 | ]; 37 | -------------------------------------------------------------------------------- /dist/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/usePromiseMatcher.d.ts: -------------------------------------------------------------------------------- 1 | import { PromiseLoader, UsePromise } from "./types"; 2 | export declare const usePromise: (loaderFn: PromiseLoader) => UsePromise; 3 | -------------------------------------------------------------------------------- /dist/usePromiseMatcher.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | import * as React from "react"; 11 | import { PromiseLoading } from "./PromiseLoading"; 12 | import { PromiseRejected } from "./PromiseRejected"; 13 | import { PromiseResolved } from "./PromiseResolved"; 14 | import { PromiseIdle } from "./PromiseIdle"; 15 | import { flushSync } from "react-dom"; 16 | export const usePromise = (loaderFn) => { 17 | const [result, setResult] = React.useState(new PromiseIdle()); 18 | const load = React.useCallback((...args) => __awaiter(void 0, void 0, void 0, function* () { 19 | setResult(new PromiseLoading()); 20 | try { 21 | const data = yield loaderFn(...args); 22 | flushSync(() => setResult(new PromiseResolved(data))); 23 | } 24 | catch (err) { 25 | flushSync(() => setResult(new PromiseRejected(err))); 26 | } 27 | }), [loaderFn]); 28 | const clear = () => setResult(new PromiseIdle()); 29 | return [result, load, clear]; 30 | }; 31 | -------------------------------------------------------------------------------- /dist/usePromiseWithInterval.d.ts: -------------------------------------------------------------------------------- 1 | import { PromiseLoader, UsePromiseWithInterval } from "./types"; 2 | export declare const usePromiseWithInterval: (loaderFn: PromiseLoader, interval: number) => UsePromiseWithInterval; 3 | -------------------------------------------------------------------------------- /dist/usePromiseWithInterval.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | import { useCallback, useEffect, useRef, useState } from "react"; 11 | import { usePromise } from "./usePromiseMatcher"; 12 | export const usePromiseWithInterval = (loaderFn, interval) => { 13 | const [result, load, reset] = usePromise(loaderFn); 14 | const [tryCount, setTryCount] = useState(0); 15 | const increment = useCallback(() => { 16 | setTryCount((v) => v + 1); 17 | }, [setTryCount]); 18 | const timer = useRef(undefined); 19 | const start = useCallback((...args) => { 20 | timer.current = setTimeout(function tick() { 21 | return __awaiter(this, void 0, void 0, function* () { 22 | yield load(...args); 23 | timer.current = setTimeout(tick, interval); 24 | }); 25 | }, interval); 26 | }, [load, interval, timer]); 27 | useEffect(() => { 28 | if (result.isLoading) { 29 | increment(); 30 | } 31 | }, [result, increment]); 32 | const stop = useCallback(() => { 33 | clearTimeout(timer.current); 34 | }, [timer]); 35 | useEffect(() => { 36 | return () => { 37 | clearTimeout(timer.current); 38 | timer.current = undefined; 39 | }; 40 | }, [timer]); 41 | return [result, start, stop, load, reset, tryCount]; 42 | }; 43 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "jsdom" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-use-promise-matcher", 3 | "version": "1.5.0", 4 | "description": "React hooks library for handling promise states in a functional way", 5 | "homepage": "https://github.com/softwaremill/react-use-promise-matcher", 6 | "bugs": { 7 | "url": "https://github.com/softwaremill/react-use-promise-matcher/issues", 8 | "email": "frontend-dev@softwaremill.com" 9 | }, 10 | "type": "module", 11 | "main": "dist/index-commonjs.cjs", 12 | "module": "dist/index.mjs", 13 | "types": "dist/index.d.ts", 14 | "scripts": { 15 | "clean": "rimraf dist", 16 | "build": "rollup -c && tsc", 17 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 18 | "test-ci": "jest --ci", 19 | "test": "jest --debug --watch", 20 | "test-once": "jest", 21 | "commit": "git-cz" 22 | }, 23 | "author": { 24 | "name": "Piotr Majcher", 25 | "email": "majcherpiotr.dev@gmail.com", 26 | "url": "https://github.com/majcherpiotrek" 27 | }, 28 | "contributors": [ 29 | { 30 | "name": "Jakub Antolak", 31 | "email": "poprostuantolak@gmail.com", 32 | "url": "https://github.com/afternoon2" 33 | } 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/softwaremill/react-use-promise-matcher.git" 38 | }, 39 | "license": "MIT", 40 | "devDependencies": { 41 | "@testing-library/jest-dom": "^5.16.5", 42 | "@testing-library/react": "^13.4.0", 43 | "@types/jest": "^29.1.2", 44 | "@types/react": "^18.0.21", 45 | "@types/react-dom": "^18.0.6", 46 | "@types/testing-library__jest-dom": "^5.9.1", 47 | "@types/testing-library__react": "^10.0.1", 48 | "@typescript-eslint/eslint-plugin": "^5.3.0", 49 | "@typescript-eslint/parser": "5.3.0", 50 | "cz-conventional-changelog": "3.1.0", 51 | "eslint": "^8.1.0", 52 | "eslint-config-airbnb": "^18.1.0", 53 | "eslint-config-prettier": "^6.10.1", 54 | "eslint-import-resolver-typescript": "^2.0.0", 55 | "eslint-plugin-import": "^2.20.1", 56 | "eslint-plugin-json": "^2.1.1", 57 | "eslint-plugin-jsx-a11y": "^6.2.3", 58 | "eslint-plugin-prettier": "^3.1.2", 59 | "eslint-plugin-react": "^7.19.0", 60 | "husky": "^4.2.3", 61 | "jest": "^29.1.2", 62 | "jest-environment-jsdom": "^29.1.2", 63 | "prettier": "^2.4.1", 64 | "react": "^18.2.0", 65 | "react-dom": "^18.2.0", 66 | "rimraf": "^3.0.2", 67 | "rollup": "^2.3.2", 68 | "rollup-plugin-typescript2": "^0.27.0", 69 | "ts-jest": "^29.0.3", 70 | "ts-loader": "^6.2.1", 71 | "typescript": "^4.4.4", 72 | "webpack": "^4.42.0", 73 | "webpack-cli": "^3.3.11" 74 | }, 75 | "peerDependencies": { 76 | "react": "^16.13.1" 77 | }, 78 | "config": { 79 | "commitizen": { 80 | "path": "./node_modules/cz-conventional-changelog" 81 | } 82 | }, 83 | "husky": { 84 | "hooks": { 85 | "pre-commit": "yarn lint", 86 | "pre-push": "yarn test-once && yarn lint" 87 | } 88 | }, 89 | "files": [ 90 | "dist", 91 | "README.md", 92 | "CHANGELOG.md" 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import pkg from "./package.json"; 3 | 4 | export default { 5 | input: `src/index.ts`, 6 | output: [ 7 | { 8 | file: pkg.module, 9 | format: "es", 10 | sourcemap: true, 11 | globals: { 12 | react: "React", 13 | }, 14 | entryFileNames: "[name].mjs", 15 | }, 16 | { 17 | file: pkg.main, 18 | format: "cjs", 19 | sourcemap: true, 20 | globals: { 21 | react: "React", 22 | }, 23 | entryFileNames: "[name].cjs", 24 | }, 25 | ], 26 | // Indicate here external modules you don't want to include in your bundle (i.e.: 'lodash') 27 | external: ["react"], 28 | 29 | plugins: [typescript({ useTsconfigDeclarationDir: true })], 30 | }; 31 | -------------------------------------------------------------------------------- /src/PromiseIdle.test.ts: -------------------------------------------------------------------------------- 1 | import { PromiseMatcher, PromiseResultShape } from "./types"; 2 | import { PromiseIdle } from "./PromiseIdle"; 3 | 4 | // eslint-disable-rule no-unused-vars 5 | describe("PromiseIdle", () => { 6 | const IDLE_TEXT = "Idle"; 7 | const LOADING_TEXT = "Loading..."; 8 | 9 | const matcherNoIdle: PromiseMatcher = { 10 | Loading: () => LOADING_TEXT, 11 | Rejected: () => "rejected", 12 | Resolved: () => "resolved", 13 | }; 14 | 15 | const matcher: PromiseMatcher = { 16 | ...matcherNoIdle, 17 | Idle: () => IDLE_TEXT, 18 | }; 19 | 20 | it("isIdle on PromiseIdle should be true", () => { 21 | const { isIdle, isLoading, isRejected, isResolved } = new PromiseIdle(); 22 | expect(isIdle).toBe(true); 23 | expect(isLoading).toBe(false); 24 | expect(isRejected).toBe(false); 25 | expect(isResolved).toBe(false); 26 | }); 27 | 28 | it("calling match on PromiseIdle with provided matcher should return 'Idle' text", () => 29 | expect(new PromiseIdle().match(matcher)).toBe(IDLE_TEXT)); 30 | 31 | it("calling match on PromiseIdle without Idle matcher should return 'Loading...' text", () => 32 | expect(new PromiseIdle().match(matcherNoIdle)).toBe(LOADING_TEXT)); 33 | 34 | it("calling map on PromiseIdle with provided mapper should return new PromiseIdle instance", () => { 35 | const original: PromiseResultShape = new PromiseIdle(); 36 | const mapped: PromiseResultShape = original.map((n) => `${n}`); 37 | expect(original).toBeInstanceOf(PromiseIdle); 38 | expect(mapped).toBeInstanceOf(PromiseIdle); 39 | }); 40 | 41 | it("calling mapErr on PromiseIdle with provided mapper should return new PromiseIdle instance", () => { 42 | const original: PromiseResultShape = new PromiseIdle(); 43 | const mapped: PromiseResultShape = original.mapErr((err) => err.message); 44 | expect(original).toBeInstanceOf(PromiseIdle); 45 | expect(mapped).toBeInstanceOf(PromiseIdle); 46 | }); 47 | 48 | it("calling get on PromiseIdle should throw an Error", () => 49 | expect(() => new PromiseIdle().get()).toThrow(new Error("Cannot get the value while the Promise is idle"))); 50 | 51 | it("calling getOr on PromiseIdle should return 'some alternative' text", () => { 52 | const alternativeText = "some alternative"; 53 | expect(new PromiseIdle().getOr(alternativeText)).toBe(alternativeText); 54 | }); 55 | 56 | it("calling flatMap on PromiseIdle with provided mapper should return new PromiseIdle instance", () => { 57 | const original: PromiseResultShape = new PromiseIdle(); 58 | const mapped: PromiseResultShape = original.flatMap(() => new PromiseIdle()); 59 | expect(original).toBeInstanceOf(PromiseIdle); 60 | expect(mapped).toBeInstanceOf(PromiseIdle); 61 | }); 62 | 63 | it("calling onResolved on PromiseIdle should not invoke the callback", () => { 64 | const callback = jest.fn(); 65 | const promiseIdle = new PromiseIdle(); 66 | promiseIdle.onResolved(callback); 67 | expect(callback).not.toHaveBeenCalled(); 68 | }); 69 | 70 | it("calling onLoading on PromiseResolved should not invoke provided callback", () => { 71 | const callback = jest.fn(); 72 | new PromiseIdle().onLoading(callback); 73 | expect(callback).not.toHaveBeenCalled(); 74 | }); 75 | 76 | it("calling onRejected on PromiseIdle should not invoke provided callback", () => { 77 | const callback = jest.fn(); 78 | new PromiseIdle().onRejected(callback); 79 | expect(callback).not.toHaveBeenCalled(); 80 | }); 81 | 82 | it("calling onIdle on PromiseIdle should invoke provided callback", () => { 83 | const callback = jest.fn(); 84 | new PromiseIdle().onIdle(callback); 85 | expect(callback).toHaveBeenCalledTimes(1); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/PromiseIdle.ts: -------------------------------------------------------------------------------- 1 | import { PromiseMatcher, PromiseResultShape } from "./types"; 2 | 3 | export class PromiseIdle implements PromiseResultShape { 4 | public isIdle = true; 5 | public isLoading = false; 6 | public isResolved = false; 7 | public isRejected = false; 8 | 9 | public match = (matcher: PromiseMatcher): U => (matcher.Idle ? matcher.Idle() : matcher.Loading()); 10 | 11 | public map = (): PromiseResultShape => new PromiseIdle(); 12 | 13 | public flatMap = (): PromiseResultShape => new PromiseIdle(); 14 | 15 | public mapErr = (): PromiseResultShape => new PromiseIdle(); 16 | 17 | public get = (): T => { 18 | throw new Error("Cannot get the value while the Promise is idle"); 19 | }; 20 | 21 | public getOr = (orValue: T): T => orValue; 22 | 23 | public onResolved = (_: (value: T) => unknown) => { 24 | return this; 25 | }; 26 | 27 | public onRejected = (_: (err: E) => unknown) => { 28 | return this; 29 | }; 30 | 31 | public onLoading = (_: () => unknown) => { 32 | return this; 33 | }; 34 | 35 | public onIdle = (fn: () => unknown) => { 36 | fn(); 37 | return this; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/PromiseLoading.test.ts: -------------------------------------------------------------------------------- 1 | import { PromiseLoading } from "./PromiseLoading"; 2 | import { PromiseMatcher, PromiseResultShape } from "./types"; 3 | import { PromiseResolved } from "./PromiseResolved"; 4 | 5 | describe("PromiseLoading", () => { 6 | const LOADING_TEXT = "Loading..."; 7 | 8 | const matcher: PromiseMatcher = { 9 | Idle: () => "idle", 10 | Loading: () => LOADING_TEXT, 11 | Rejected: () => "rejected", 12 | Resolved: () => "resolved", 13 | }; 14 | 15 | it("isLoading on PromiseLoading should be true", () => { 16 | const { isIdle, isLoading, isRejected, isResolved } = new PromiseLoading(); 17 | expect(isIdle).toBe(false); 18 | expect(isLoading).toBe(true); 19 | expect(isRejected).toBe(false); 20 | expect(isResolved).toBe(false); 21 | }); 22 | 23 | it("calling match on PromiseLoading with provided matcher should return 'Loading...' text", () => 24 | expect(new PromiseLoading().match(matcher)).toBe(LOADING_TEXT)); 25 | 26 | it("calling map on PromiseLoading with provided mapper should return new PromiseLoading instance", () => { 27 | const original: PromiseResultShape = new PromiseLoading(); 28 | const mapped: PromiseResultShape = original.map((n) => `${n}`); 29 | expect(original).toBeInstanceOf(PromiseLoading); 30 | expect(mapped).toBeInstanceOf(PromiseLoading); 31 | }); 32 | 33 | it("calling mapErr on PromiseLoading with provided mapper should return new PromiseLoading instance", () => { 34 | const original: PromiseResultShape = new PromiseLoading(); 35 | const mapped: PromiseResultShape = original.mapErr((err) => err.message); 36 | expect(original).toBeInstanceOf(PromiseLoading); 37 | expect(mapped).toBeInstanceOf(PromiseLoading); 38 | }); 39 | 40 | it("calling get on PromiseLoading should throw an Error", () => 41 | expect(() => new PromiseLoading().get()).toThrow( 42 | new Error("Cannot get the value while the Promise is loading"), 43 | )); 44 | 45 | it("calling getOr on PromiseLoading should return 'some alternative' text", () => { 46 | const alternativeText = "some alternative"; 47 | expect(new PromiseLoading().getOr(alternativeText)).toBe(alternativeText); 48 | }); 49 | 50 | it("calling flatMap on PromiseLoading with provided mapper should return new PromiseLoading instance", () => { 51 | const original: PromiseResultShape = new PromiseLoading(); 52 | const mapped: PromiseResultShape = original.flatMap((n) => new PromiseResolved(`${n}`)); 53 | expect(original).toBeInstanceOf(PromiseLoading); 54 | expect(mapped).toBeInstanceOf(PromiseLoading); 55 | }); 56 | 57 | it("calling onResolved on PromiseLoading should not invoke provided callback", () => { 58 | const callback = jest.fn(); 59 | new PromiseLoading().onResolved(callback); 60 | expect(callback).not.toHaveBeenCalled(); 61 | }); 62 | 63 | it("calling onRejected on PromiseResolved should not invoke provided callback", () => { 64 | const callback = jest.fn(); 65 | new PromiseLoading().onRejected(callback); 66 | expect(callback).not.toHaveBeenCalled(); 67 | }); 68 | 69 | it("calling onLoading on PromiseLoading should invoke provided callback", () => { 70 | const callback = jest.fn(); 71 | new PromiseLoading().onLoading(callback); 72 | expect(callback).toHaveBeenCalledTimes(1); 73 | }); 74 | 75 | it("calling onIdle on PromiseLoading should not invoke provided callback", () => { 76 | const callback = jest.fn(); 77 | new PromiseLoading().onIdle(callback); 78 | expect(callback).not.toHaveBeenCalled(); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/PromiseLoading.ts: -------------------------------------------------------------------------------- 1 | import { PromiseMatcher, PromiseResultShape } from "./types"; 2 | 3 | export class PromiseLoading implements PromiseResultShape { 4 | public isIdle = false; 5 | public isLoading = true; 6 | public isResolved = false; 7 | public isRejected = false; 8 | 9 | public match = (matcher: PromiseMatcher): U => matcher.Loading(); 10 | 11 | public map = (): PromiseResultShape => new PromiseLoading(); 12 | 13 | public flatMap = (): PromiseResultShape => new PromiseLoading(); 14 | 15 | public mapErr = (): PromiseResultShape => new PromiseLoading(); 16 | 17 | public get = (): T => { 18 | throw new Error("Cannot get the value while the Promise is loading"); 19 | }; 20 | 21 | public getOr = (orValue: T): T => orValue; 22 | 23 | public onResolved = (_: (value: T) => unknown) => { 24 | return this; 25 | }; 26 | 27 | public onRejected = (_: (err: E) => unknown) => { 28 | return this; 29 | }; 30 | 31 | public onLoading = (fn: () => unknown) => { 32 | fn(); 33 | return this; 34 | }; 35 | 36 | public onIdle = (_: () => unknown) => { 37 | return this; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/PromiseRejected.test.ts: -------------------------------------------------------------------------------- 1 | import { PromiseRejected, isPromiseRejected } from "./PromiseRejected"; 2 | import { PromiseMatcher, PromiseResultShape } from "./types"; 3 | import { PromiseIdle } from "./PromiseIdle"; 4 | import { PromiseLoading } from "./PromiseLoading"; 5 | import { PromiseResolved } from "./PromiseResolved"; 6 | 7 | describe("PromiseRejected", () => { 8 | const REJECTION_REASON = "rejection reason"; 9 | const REJECTED = "rejected"; 10 | 11 | const matcher: PromiseMatcher = { 12 | Idle: () => "idle", 13 | Loading: () => "loading", 14 | Rejected: () => REJECTED, 15 | Resolved: () => "resolved", 16 | }; 17 | 18 | it("isRejected on PromiseRejected should be true", () => { 19 | const { isIdle, isLoading, isRejected, isResolved } = new PromiseRejected(REJECTION_REASON); 20 | expect(isIdle).toBe(false); 21 | expect(isLoading).toBe(false); 22 | expect(isRejected).toBe(true); 23 | expect(isResolved).toBe(false); 24 | }); 25 | 26 | it("calling match on PromiseRejected with provided matcher should return 'rejected' text", () => 27 | expect(new PromiseRejected(new Error(REJECTION_REASON)).match(matcher)).toBe(REJECTED)); 28 | 29 | it("calling map on PromiseRejected with provided mapper should return new PromiseRejected instance", () => { 30 | const original: PromiseResultShape = new PromiseRejected( 31 | new Error(REJECTION_REASON), 32 | ); 33 | const mapped: PromiseResultShape = original.map((n) => `${n}`); 34 | expect(original).toBeInstanceOf(PromiseRejected); 35 | expect(mapped).toBeInstanceOf(PromiseRejected); 36 | }); 37 | 38 | it("calling get on PromiseRejected should throw an Error", () => 39 | expect(() => new PromiseRejected(new Error(REJECTION_REASON)).get()).toThrow(new Error(REJECTION_REASON))); 40 | 41 | it("calling mapErr on PromiseRejected with provided mapper should map error and mapped error should be thrown when get called", () => { 42 | const original: PromiseResultShape = new PromiseRejected( 43 | new Error(REJECTION_REASON), 44 | ); 45 | const mapError = (err: Error) => `${err.message} was mapped`; 46 | const mapped: PromiseResultShape = original.mapErr(mapError); 47 | expect(original).toBeInstanceOf(PromiseRejected); 48 | expect(mapped).toBeInstanceOf(PromiseRejected); 49 | expect(() => mapped.get()).toThrow(`${REJECTION_REASON} was mapped`); 50 | }); 51 | 52 | it("calling getOr on PromiseRejected should return 'some alternative' text", () => { 53 | const alternativeText = "some alternative"; 54 | expect(new PromiseRejected(REJECTION_REASON).getOr(alternativeText)).toBe(alternativeText); 55 | }); 56 | 57 | it("calling isPromiseRejected allows asserting PromiseRejected type and accessing the rejection reason safely", () => { 58 | const promiseRejected: PromiseResultShape = new PromiseRejected( 59 | REJECTION_REASON, 60 | ); 61 | 62 | if (isPromiseRejected(promiseRejected)) { 63 | expect(promiseRejected.reason).toEqual(REJECTION_REASON); 64 | } else { 65 | fail("The PromiseRejected type was not correctly detected"); 66 | } 67 | }); 68 | 69 | it("calling isPromiseRejected on PromiseResolved returns false", () => { 70 | expect(isPromiseRejected(new PromiseResolved("some value"))).toEqual(false); 71 | }); 72 | 73 | it("calling isPromiseRejected on PromiseIdle returns false", () => { 74 | expect(isPromiseRejected(new PromiseIdle())).toEqual(false); 75 | }); 76 | 77 | it("calling isPromiseRejected on PromiseLoading returns false", () => { 78 | expect(isPromiseRejected(new PromiseLoading())).toEqual(false); 79 | }); 80 | 81 | it("calling flatMap on PromiseRejected with provided mapper should return new PromiseRejected instance", () => { 82 | const original: PromiseResultShape = new PromiseRejected( 83 | new Error(REJECTION_REASON), 84 | ); 85 | const mapped: PromiseResultShape = original.flatMap((n) => new PromiseResolved(`${n}`)); 86 | expect(original).toBeInstanceOf(PromiseRejected); 87 | expect(mapped).toBeInstanceOf(PromiseRejected); 88 | }); 89 | 90 | it("calling onResolved on PromiseRejected should not invoke the callback", () => { 91 | const callback = jest.fn(); 92 | new PromiseRejected(REJECTION_REASON).onResolved(callback); 93 | expect(callback).not.toHaveBeenCalled(); 94 | }); 95 | 96 | it("calling onLoading on PromiseRejected should not invoke provided callback", () => { 97 | const callback = jest.fn(); 98 | new PromiseRejected(REJECTION_REASON).onLoading(callback); 99 | expect(callback).not.toHaveBeenCalled(); 100 | }); 101 | 102 | it("calling onRejected on PromiseRejected should invoke provided callback", () => { 103 | const callback = jest.fn(); 104 | new PromiseRejected(REJECTION_REASON).onRejected(callback); 105 | expect(callback).toHaveBeenCalledTimes(1); 106 | }); 107 | 108 | it("calling onIdle on PromiseRejected should not invoke provided callback", () => { 109 | const callback = jest.fn(); 110 | new PromiseRejected(REJECTION_REASON).onIdle(callback); 111 | expect(callback).not.toHaveBeenCalled(); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/PromiseRejected.ts: -------------------------------------------------------------------------------- 1 | import { PromiseMatcher, PromiseResultShape } from "./types"; 2 | 3 | export class PromiseRejected implements PromiseResultShape { 4 | public isIdle = false; 5 | public isLoading = false; 6 | public isResolved = false; 7 | public isRejected = true; 8 | constructor(public reason: E) {} 9 | 10 | public match = (matcher: PromiseMatcher): U => matcher.Rejected(this.reason); 11 | 12 | public map = (): PromiseResultShape => new PromiseRejected(this.reason); 13 | 14 | public flatMap = (): PromiseResultShape => new PromiseRejected(this.reason); 15 | 16 | public mapErr = (fn: (err: E) => U): PromiseResultShape => new PromiseRejected(fn(this.reason)); 17 | 18 | public get = (): T => { 19 | throw this.reason; 20 | }; 21 | 22 | public getOr = (orValue: T): T => orValue; 23 | 24 | public onResolved = (_: (value: T) => unknown) => { 25 | return this; 26 | }; 27 | 28 | public onRejected = (fn: (err: E) => unknown) => { 29 | fn(this.reason); 30 | return this; 31 | }; 32 | 33 | public onLoading = (_: () => unknown) => { 34 | return this; 35 | }; 36 | 37 | public onIdle = (_: () => unknown) => { 38 | return this; 39 | }; 40 | } 41 | 42 | export const isPromiseRejected = ( 43 | promiseResultShape: PromiseResultShape, 44 | ): promiseResultShape is PromiseRejected => promiseResultShape.isRejected; 45 | -------------------------------------------------------------------------------- /src/PromiseResolved.test.ts: -------------------------------------------------------------------------------- 1 | import { PromiseResolved, isPromiseResolved } from "./PromiseResolved"; 2 | import { PromiseMatcher, PromiseResultShape } from "./types"; 3 | import { PromiseRejected } from "./PromiseRejected"; 4 | import { PromiseIdle } from "./PromiseIdle"; 5 | import { PromiseLoading } from "./PromiseLoading"; 6 | 7 | interface TestInterface { 8 | value: string; 9 | } 10 | 11 | describe("PromiseResolved", () => { 12 | const RESOLVED_VALUE = "resolved value"; 13 | const RESOLVED_OBJECT: TestInterface = { 14 | value: RESOLVED_VALUE, 15 | }; 16 | 17 | const matcher: PromiseMatcher = { 18 | Idle: () => "idle", 19 | Loading: () => "loading", 20 | Rejected: () => "rejected", 21 | Resolved: (obj) => obj.value, 22 | }; 23 | 24 | it("isResolved on PromiseResolved should be true", () => { 25 | const { isIdle, isLoading, isRejected, isResolved } = new PromiseResolved(RESOLVED_OBJECT); 26 | expect(isIdle).toBe(false); 27 | expect(isLoading).toBe(false); 28 | expect(isRejected).toBe(false); 29 | expect(isResolved).toBe(true); 30 | }); 31 | 32 | it("calling match on PromiseResolved with provided matcher should return 'resolved value' text", () => 33 | expect(new PromiseResolved(RESOLVED_OBJECT).match(matcher)).toBe(RESOLVED_VALUE)); 34 | 35 | it("calling map on PromiseResolved with provided mapper should return new PromiseResolved instance with mapped value", () => { 36 | const original: PromiseResultShape = new PromiseResolved( 37 | RESOLVED_OBJECT, 38 | ); 39 | const mapped: PromiseResultShape = original.map((obj) => `${obj.value} was mapped`); 40 | expect(original).toBeInstanceOf(PromiseResolved); 41 | expect(mapped).toBeInstanceOf(PromiseResolved); 42 | expect(mapped.get()).toBe(`${RESOLVED_OBJECT.value} was mapped`); 43 | }); 44 | 45 | it("calling mapErr on PromiseResolved with provided mapper should return new PromiseResolved instance", () => { 46 | const original: PromiseResultShape = new PromiseResolved( 47 | RESOLVED_OBJECT, 48 | ); 49 | const mapped: PromiseResultShape = original.mapErr((err) => err.message); 50 | expect(original).toBeInstanceOf(PromiseResolved); 51 | expect(mapped).toBeInstanceOf(PromiseResolved); 52 | }); 53 | 54 | it("calling get on PromiseResolved should return value", () => 55 | expect(new PromiseResolved(RESOLVED_OBJECT).get()).toBe(RESOLVED_OBJECT)); 56 | 57 | it("calling getOr on PromiseResolved should return value", () => { 58 | const alternativeText = "some alternative"; 59 | const promiseResolved: PromiseResultShape = new PromiseResolved(RESOLVED_VALUE); 60 | expect(promiseResolved.getOr(alternativeText)).toBe(RESOLVED_VALUE); 61 | }); 62 | 63 | it("calling isPromiseResolved on a resolved promise allows asserting PromiseResolved type and accessing the value safely", () => { 64 | const promiseResolved: PromiseResultShape = new PromiseResolved(RESOLVED_VALUE); 65 | 66 | if (isPromiseResolved(promiseResolved)) { 67 | expect(promiseResolved.value).toEqual(RESOLVED_VALUE); 68 | } else { 69 | fail("The PromiseResolved type was not correctly detected"); 70 | } 71 | }); 72 | 73 | it("calling isPromiseResolved on PromiseRejected returns false", () => { 74 | expect(isPromiseResolved(new PromiseRejected("some error"))).toEqual(false); 75 | }); 76 | 77 | it("calling isPromiseResolved on PromiseIdle returns false", () => { 78 | expect(isPromiseResolved(new PromiseIdle())).toEqual(false); 79 | }); 80 | 81 | it("calling isPromiseResolved on PromiseLoading returns false", () => { 82 | expect(isPromiseResolved(new PromiseLoading())).toEqual(false); 83 | }); 84 | 85 | it("calling flatMap on PromiseResolved with provided mapper should return new PromiseResolved instance with mapped value", () => { 86 | const original: PromiseResultShape = new PromiseResolved( 87 | RESOLVED_OBJECT, 88 | ); 89 | const mapped: PromiseResultShape = original.flatMap( 90 | (obj) => new PromiseResolved(`${obj.value} was mapped`), 91 | ); 92 | expect(original).toBeInstanceOf(PromiseResolved); 93 | expect(mapped).toBeInstanceOf(PromiseResolved); 94 | expect(mapped.get()).toBe(`${RESOLVED_OBJECT.value} was mapped`); 95 | }); 96 | 97 | it("calling flatMap on PromiseResolved with provided mapper should return new PromiseRejected instance with an error from the second promise", () => { 98 | const original: PromiseResultShape = new PromiseResolved( 99 | RESOLVED_OBJECT, 100 | ); 101 | const mapped: PromiseResultShape = original.flatMap( 102 | () => new PromiseRejected(new Error("some error")), 103 | ); 104 | expect(original).toBeInstanceOf(PromiseResolved); 105 | expect(mapped).toBeInstanceOf(PromiseRejected); 106 | expect((mapped as PromiseRejected).reason.message).toBe("some error"); 107 | }); 108 | 109 | it("calling flatMap on PromiseResolved with provided mapper should return new PromiseLoading instance", () => { 110 | const original: PromiseResultShape = new PromiseResolved( 111 | RESOLVED_OBJECT, 112 | ); 113 | const mapped: PromiseResultShape = original.flatMap(() => new PromiseLoading()); 114 | expect(original).toBeInstanceOf(PromiseResolved); 115 | expect(mapped).toBeInstanceOf(PromiseLoading); 116 | }); 117 | 118 | it("calling flatMap on PromiseResolved with provided mapper should return new PromiseIdle instance", () => { 119 | const original: PromiseResultShape = new PromiseResolved( 120 | RESOLVED_OBJECT, 121 | ); 122 | const mapped: PromiseResultShape = original.flatMap(() => new PromiseIdle()); 123 | expect(original).toBeInstanceOf(PromiseResolved); 124 | expect(mapped).toBeInstanceOf(PromiseIdle); 125 | }); 126 | 127 | it("calling onResolved on PromiseResolved should invoke provided callback", () => { 128 | const callback = jest.fn(); 129 | new PromiseResolved(RESOLVED_VALUE).onResolved(callback); 130 | expect(callback).toHaveBeenCalledTimes(1); 131 | }); 132 | 133 | it("calling onRejected on PromiseResolved should not invoke provided callback", () => { 134 | const callback = jest.fn(); 135 | new PromiseResolved(RESOLVED_VALUE).onRejected(callback); 136 | expect(callback).not.toHaveBeenCalled(); 137 | }); 138 | 139 | it("calling onLoading on PromiseResolved should not invoke provided callback", () => { 140 | const callback = jest.fn(); 141 | new PromiseResolved(RESOLVED_VALUE).onLoading(callback); 142 | expect(callback).not.toHaveBeenCalled(); 143 | }); 144 | 145 | it("calling onIdle on PromiseResolved should not invoke provided callback", () => { 146 | const callback = jest.fn(); 147 | new PromiseResolved(RESOLVED_VALUE).onIdle(callback); 148 | expect(callback).not.toHaveBeenCalled(); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /src/PromiseResolved.ts: -------------------------------------------------------------------------------- 1 | import { PromiseMatcher, PromiseResultShape } from "./types"; 2 | 3 | export class PromiseResolved implements PromiseResultShape { 4 | public isIdle = false; 5 | public isLoading = false; 6 | public isResolved = true; 7 | public isRejected = false; 8 | 9 | constructor(public value: T) {} 10 | 11 | public match = (matcher: PromiseMatcher): U => matcher.Resolved(this.value); 12 | 13 | public map = (fn: (value: T) => U): PromiseResultShape => new PromiseResolved(fn(this.value)); 14 | 15 | public flatMap = (fn: (value: T) => PromiseResultShape): PromiseResultShape => fn(this.value); 16 | 17 | public mapErr = (): PromiseResultShape => new PromiseResolved(this.value); 18 | 19 | public get = (): T => { 20 | return this.value; 21 | }; 22 | 23 | public getOr = (): T => this.get(); 24 | 25 | public onResolved = (fn: (value: T) => unknown) => { 26 | fn(this.get()); 27 | return this; 28 | }; 29 | 30 | public onRejected = (_: (err: E) => unknown) => { 31 | return this; 32 | }; 33 | 34 | public onLoading = (_: () => unknown) => { 35 | return this; 36 | }; 37 | 38 | public onIdle = (_: () => unknown) => { 39 | return this; 40 | }; 41 | } 42 | 43 | export const isPromiseResolved = ( 44 | promiseResultShape: PromiseResultShape, 45 | ): promiseResultShape is PromiseResolved => promiseResultShape.isResolved; 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { usePromise } from "./usePromiseMatcher"; 2 | export { usePromiseWithInterval } from "./usePromiseWithInterval"; 3 | export { PromiseIdle } from "./PromiseIdle"; 4 | export { PromiseLoading } from "./PromiseLoading"; 5 | export { PromiseRejected, isPromiseRejected } from "./PromiseRejected"; 6 | export { PromiseResolved, isPromiseResolved } from "./PromiseResolved"; 7 | export { PromiseMatcher, UsePromise, PromiseResultShape, PromiseLoader, UsePromiseWithInterval } from "./types"; 8 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface PromiseMatcher { 2 | Resolved: (value: T) => U; 3 | Rejected: (reason: E) => U; 4 | Loading: () => U; 5 | Idle?: () => U; 6 | } 7 | 8 | export interface PromiseResultShape { 9 | match: (matcher: PromiseMatcher) => U; 10 | map: (fn: (value: T) => U) => PromiseResultShape; 11 | flatMap: (fn: (value: T) => PromiseResultShape) => PromiseResultShape; 12 | mapErr: (fn: (err: E) => U) => PromiseResultShape; 13 | get: () => T; 14 | getOr: (orValue: T) => T; 15 | onResolved: (fn: (value: T) => unknown) => PromiseResultShape; 16 | onRejected: (fn: (err: E) => unknown) => PromiseResultShape; 17 | onLoading: (fn: () => unknown) => PromiseResultShape; 18 | onIdle: (fn: () => unknown) => PromiseResultShape; 19 | isIdle: boolean; 20 | isLoading: boolean; 21 | isResolved: boolean; 22 | isRejected: boolean; 23 | } 24 | 25 | export type PromiseLoader = (...args: Args) => Promise; 26 | export type UsePromise = [ 27 | result: PromiseResultShape, 28 | load: PromiseLoader, 29 | reset: () => void, 30 | ]; 31 | export type UsePromiseWithInterval = [ 32 | result: PromiseResultShape, 33 | start: (...args: A) => void, 34 | stop: () => void, 35 | load: PromiseLoader, 36 | reset: () => void, 37 | tryCount: number, 38 | ]; 39 | -------------------------------------------------------------------------------- /src/usePromiseMatcher.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render, fireEvent, waitFor } from "@testing-library/react"; 3 | import { usePromise } from "./usePromiseMatcher"; 4 | import "@testing-library/jest-dom/extend-expect"; 5 | 6 | interface TestData { 7 | data: string; 8 | } 9 | 10 | interface TestComponent { 11 | loader: () => Promise; 12 | } 13 | 14 | interface Params { 15 | param: string; 16 | } 17 | 18 | interface TestComponentWithArguments { 19 | loader: (params: Params) => Promise; 20 | } 21 | 22 | interface TestComponentWithCallbackArguments { 23 | loader: (params: Params) => Promise; 24 | onIdle: () => void; 25 | onLoading: () => void; 26 | onRejected: () => void; 27 | onResolved: () => void; 28 | } 29 | 30 | const IDLE_MESSAGE = "Waiting for call"; 31 | const LOADING_MESSAGE = "Loading"; 32 | const ERROR_MESSAGE = "Promise was rejected"; 33 | const SAMPLE_TEXT = "Some asynchronously loaded text"; 34 | 35 | const testData: TestData = { data: SAMPLE_TEXT }; 36 | const containerId = "container"; 37 | const loadButtonId = "loadButton"; 38 | const clearButtonId = "clearButton"; 39 | 40 | const TestComponent: React.FC = ({ loader }: TestComponent) => { 41 | const [result, load, clear] = usePromise(loader); 42 | return ( 43 |
44 | {result.match({ 45 | Idle: () => IDLE_MESSAGE, 46 | Loading: () => LOADING_MESSAGE, 47 | Rejected: (err) => err, 48 | Resolved: (res) => res.data, 49 | })} 50 | 53 | 56 |
57 | ); 58 | }; 59 | 60 | const TestComponentWithArguments: React.FC = ({ loader }: TestComponentWithArguments) => { 61 | const [result, load, clear] = usePromise(loader); 62 | 63 | const onClick = () => load({ param: SAMPLE_TEXT }); 64 | 65 | return ( 66 |
67 | {result.match({ 68 | Idle: () => IDLE_MESSAGE, 69 | Loading: () => LOADING_MESSAGE, 70 | Rejected: (err) => err, 71 | Resolved: (res) => res.data, 72 | })} 73 | 76 | 79 |
80 | ); 81 | }; 82 | 83 | const TestComponentWithCallbacks: React.FC = ({ 84 | loader, 85 | onLoading, 86 | onRejected, 87 | onResolved, 88 | onIdle, 89 | }) => { 90 | const [result, load] = usePromise(loader); 91 | 92 | const onClick = () => load({ param: SAMPLE_TEXT }); 93 | 94 | result.onIdle(onIdle).onLoading(onLoading).onResolved(onResolved).onRejected(onRejected); 95 | 96 | return ( 97 | 100 | ); 101 | }; 102 | 103 | describe("usePromise with a no-arguments loader function", () => { 104 | const loadSomePromise = jest.fn((): Promise => Promise.resolve(testData)); 105 | const loadFailingPromise = jest.fn((): Promise => Promise.reject(ERROR_MESSAGE)); 106 | 107 | afterEach(() => { 108 | loadSomePromise.mockClear(); 109 | loadFailingPromise.mockClear(); 110 | }); 111 | 112 | it("Idle message should be rendered if the promise loader function hasn't been called yet", () => { 113 | expect(render().getByTestId(containerId)).toHaveTextContent( 114 | IDLE_MESSAGE, 115 | ); 116 | expect(loadSomePromise).toHaveBeenCalledTimes(0); 117 | }); 118 | 119 | it("Text from testData object should be rendered after the load button was clicked and promise has been resolved", async () => { 120 | const { getByTestId, findByTestId } = render(); 121 | 122 | fireEvent.click(getByTestId(loadButtonId)); 123 | expect(getByTestId(containerId)).toHaveTextContent(LOADING_MESSAGE); 124 | expect(loadSomePromise).toHaveBeenCalledTimes(1); 125 | 126 | const element = await findByTestId(containerId); 127 | 128 | expect(element).toHaveTextContent(SAMPLE_TEXT); 129 | }); 130 | 131 | it("Error message should be rendered after the promise has been rejected", async () => { 132 | const { getByTestId, findByTestId } = render(); 133 | 134 | fireEvent.click(getByTestId(loadButtonId)); 135 | expect(getByTestId(containerId)).toHaveTextContent(LOADING_MESSAGE); 136 | expect(loadFailingPromise).toHaveBeenCalledTimes(1); 137 | 138 | const element = await findByTestId(containerId); 139 | 140 | expect(element).toHaveTextContent(ERROR_MESSAGE); 141 | }); 142 | 143 | it("Idle message should be rendered after the clear function was called", async () => { 144 | const { getByTestId, findByTestId } = render(); 145 | 146 | fireEvent.click(getByTestId(loadButtonId)); 147 | expect(getByTestId(containerId)).toHaveTextContent(LOADING_MESSAGE); 148 | 149 | let element = await findByTestId(containerId); 150 | 151 | expect(element).toHaveTextContent(SAMPLE_TEXT); 152 | 153 | fireEvent.click(getByTestId(clearButtonId)); 154 | element = await findByTestId(containerId); 155 | 156 | expect(element).toHaveTextContent(IDLE_MESSAGE); 157 | }); 158 | 159 | it("should not throw `Warning: Can't perform a React state update on an unmounted component.` when Promise resolves after the component was unmounted", async () => { 160 | const consoleSpy = jest.spyOn(console, "error"); 161 | 162 | // Deferes resolving the promise until after the unmount() is called 163 | const loadDeferredPromise = jest.fn( 164 | (): Promise => new Promise((resolve) => setTimeout(() => resolve(testData), 0)), 165 | ); 166 | const { unmount, container, getByTestId } = render(); 167 | fireEvent.click(getByTestId(loadButtonId)); 168 | unmount(); 169 | 170 | // Waits for the Promise from loadDeferredPromise to resolve 171 | await new Promise((resolve) => setTimeout(() => resolve(""), 0)); 172 | 173 | expect(container.innerHTML).toEqual(""); 174 | 175 | // With unsafe state handling we would get "Warning: Can't perform a React state update on an unmounted component." in the console.error 176 | expect(consoleSpy).toHaveBeenCalledTimes(0); 177 | }); 178 | }); 179 | 180 | describe("usePromise with a loader function with arguments", () => { 181 | const loadSomePromise = jest.fn( 182 | (params: Params): Promise => Promise.resolve({ data: params.param }), 183 | ); 184 | const loadFailingPromise = jest.fn((): Promise => Promise.reject(ERROR_MESSAGE)); 185 | 186 | afterEach(() => { 187 | loadSomePromise.mockClear(); 188 | loadFailingPromise.mockClear(); 189 | }); 190 | 191 | it("Idle message should be rendered if the promise loader function hasn't been called yet", () => { 192 | expect( 193 | render().getByTestId(containerId), 194 | ).toHaveTextContent(IDLE_MESSAGE); 195 | expect(loadSomePromise).toHaveBeenCalledTimes(0); 196 | }); 197 | 198 | it("Text from testData object should be rendered after the load button was clicked and promise has been resolved", async () => { 199 | const { getByTestId, findByTestId } = render(); 200 | 201 | fireEvent.click(getByTestId(loadButtonId)); 202 | expect(getByTestId(containerId)).toHaveTextContent(LOADING_MESSAGE); 203 | expect(loadSomePromise).toHaveBeenCalledTimes(1); 204 | 205 | const element = await findByTestId(containerId); 206 | 207 | expect(element).toHaveTextContent(SAMPLE_TEXT); 208 | }); 209 | 210 | it("Error message should be rendered after the promise has been rejected", async () => { 211 | const { getByTestId, findByTestId } = render(); 212 | 213 | fireEvent.click(getByTestId(loadButtonId)); 214 | expect(getByTestId(containerId)).toHaveTextContent(LOADING_MESSAGE); 215 | expect(loadFailingPromise).toHaveBeenCalledTimes(1); 216 | 217 | const element = await findByTestId(containerId); 218 | 219 | expect(element).toHaveTextContent(ERROR_MESSAGE); 220 | }); 221 | 222 | it("Idle message should be rendered after the clear function was called", async () => { 223 | const { getByTestId, findByTestId } = render(); 224 | 225 | fireEvent.click(getByTestId(loadButtonId)); 226 | expect(getByTestId(containerId)).toHaveTextContent(LOADING_MESSAGE); 227 | 228 | let element = await findByTestId(containerId); 229 | 230 | expect(element).toHaveTextContent(SAMPLE_TEXT); 231 | 232 | fireEvent.click(getByTestId(clearButtonId)); 233 | element = await findByTestId(containerId); 234 | 235 | expect(element).toHaveTextContent(IDLE_MESSAGE); 236 | }); 237 | 238 | it("Invokes callback functions for resolved promise", async () => { 239 | const onIdle = jest.fn(); 240 | const onLoading = jest.fn(); 241 | const onRejected = jest.fn(); 242 | const onResolved = jest.fn(); 243 | 244 | const { getByTestId } = render( 245 | , 252 | ); 253 | 254 | fireEvent.click(getByTestId(loadButtonId)); 255 | 256 | expect(onIdle).toHaveBeenCalledTimes(1); 257 | expect(onLoading).toHaveBeenCalledTimes(1); 258 | 259 | await waitFor(() => { 260 | expect(onResolved).toHaveBeenCalledTimes(1); 261 | expect(onRejected).not.toHaveBeenCalled(); 262 | }); 263 | }); 264 | 265 | it("Invokes callback functions for rejected promise", async () => { 266 | const onIdle = jest.fn(); 267 | const onLoading = jest.fn(); 268 | const onRejected = jest.fn(); 269 | const onResolved = jest.fn(); 270 | 271 | const { getByTestId } = render( 272 | , 279 | ); 280 | 281 | fireEvent.click(getByTestId(loadButtonId)); 282 | 283 | expect(onIdle).toHaveBeenCalledTimes(1); 284 | expect(onLoading).toHaveBeenCalledTimes(1); 285 | 286 | await waitFor(() => { 287 | expect(onRejected).toHaveBeenCalledTimes(1); 288 | expect(onResolved).not.toHaveBeenCalled(); 289 | }); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /src/usePromiseMatcher.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PromiseLoading } from "./PromiseLoading"; 3 | import { PromiseRejected } from "./PromiseRejected"; 4 | import { PromiseResolved } from "./PromiseResolved"; 5 | import { PromiseResultShape, PromiseLoader, UsePromise } from "./types"; 6 | import { PromiseIdle } from "./PromiseIdle"; 7 | import { flushSync } from "react-dom"; 8 | 9 | export const usePromise = ( 10 | loaderFn: PromiseLoader, 11 | ): UsePromise => { 12 | const [result, setResult] = React.useState>(new PromiseIdle()); 13 | 14 | const load = React.useCallback( 15 | async (...args: Args): Promise => { 16 | setResult(new PromiseLoading()); 17 | try { 18 | const data: T = await loaderFn(...args); 19 | flushSync(() => setResult(new PromiseResolved(data))); 20 | } catch (err) { 21 | flushSync(() => setResult(new PromiseRejected(err as E))); 22 | } 23 | }, 24 | [loaderFn], 25 | ); 26 | 27 | const clear = () => setResult(new PromiseIdle()); 28 | 29 | return [result, load, clear]; 30 | }; 31 | -------------------------------------------------------------------------------- /src/usePromiseWithInterval.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render, screen, fireEvent, act } from "@testing-library/react"; 3 | import "@testing-library/jest-dom"; 4 | import { usePromiseWithInterval } from "./usePromiseWithInterval"; 5 | 6 | interface TestData { 7 | data: string; 8 | } 9 | 10 | interface TestComponentWithoutArguments { 11 | loader: () => Promise; 12 | interval: number; 13 | } 14 | 15 | const INTERVAL = 2000; 16 | const IDLE_MESSAGE = "Waiting for call"; 17 | const LOADING_MESSAGE = "Loading"; 18 | const SAMPLE_TEXT = "Some asynchronously loaded text"; 19 | 20 | const startButtonId = "startButton"; 21 | const stopButtonId = "stopButton"; 22 | const retriesParagraphId = "retries"; 23 | const retryButtonId = "retry"; 24 | 25 | const nextExpectedResult = async (result: string, unexpectedResult?: string) => { 26 | act(() => { 27 | jest.advanceTimersByTime(INTERVAL); 28 | }); 29 | 30 | if (unexpectedResult) { 31 | expect(screen.queryByText(unexpectedResult)).not.toBeInTheDocument(); 32 | } 33 | expect(await screen.findByText(result)).toBeInTheDocument(); 34 | }; 35 | 36 | const TestComponent: React.FC = ({ loader, interval }) => { 37 | const [result, start, stop, load, , tryCount] = usePromiseWithInterval(loader, interval); 38 | 39 | return ( 40 | <> 41 | {result.match({ 42 | Idle: () => IDLE_MESSAGE, 43 | Loading: () => LOADING_MESSAGE, 44 | Rejected: (err) => err, 45 | Resolved: (res) => res.data, 46 | })} 47 | 50 | 53 | 56 |

{tryCount}

57 | 58 | ); 59 | }; 60 | 61 | describe("usePromiseWithInterval with a no-arguments loader function", () => { 62 | let index = 0; 63 | 64 | const loadSomePromise = jest.fn((): Promise => { 65 | return new Promise((resolve) => { 66 | resolve({ 67 | data: `${SAMPLE_TEXT} ${index}`, 68 | }); 69 | index++; 70 | }); 71 | }); 72 | 73 | beforeEach(() => { 74 | jest.useFakeTimers(); 75 | }); 76 | 77 | afterEach(() => { 78 | loadSomePromise.mockClear(); 79 | index = 0; 80 | }); 81 | 82 | it("Should start and stop the loader execution with specified interval", async () => { 83 | render(); 84 | 85 | const startButton = screen.getByTestId(startButtonId); 86 | const stopButton = screen.getByTestId(stopButtonId); 87 | const retryButton = screen.getByTestId(retryButtonId); 88 | 89 | fireEvent.click(startButton); 90 | 91 | await nextExpectedResult(`${SAMPLE_TEXT} 0`); 92 | await nextExpectedResult(`${SAMPLE_TEXT} 1`, `${SAMPLE_TEXT} 0`); 93 | await nextExpectedResult(`${SAMPLE_TEXT} 2`, `${SAMPLE_TEXT} 1`); 94 | await nextExpectedResult(`${SAMPLE_TEXT} 3`, `${SAMPLE_TEXT} 2`); 95 | 96 | fireEvent.click(stopButton); 97 | await nextExpectedResult(`${SAMPLE_TEXT} 3`); 98 | let retriesParagraph = await screen.findByTestId(retriesParagraphId); 99 | 100 | expect(retriesParagraph.textContent).toBe("4"); 101 | 102 | fireEvent.click(retryButton); 103 | 104 | retriesParagraph = await screen.findByTestId(retriesParagraphId); 105 | expect(retriesParagraph.textContent).toBe("5"); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/usePromiseWithInterval.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useCallback, useEffect, useRef, useState } from "react"; 2 | import { PromiseLoader, UsePromiseWithInterval } from "./types"; 3 | import { usePromise } from "./usePromiseMatcher"; 4 | 5 | export const usePromiseWithInterval = ( 6 | loaderFn: PromiseLoader, 7 | interval: number, 8 | ): UsePromiseWithInterval => { 9 | const [result, load, reset] = usePromise(loaderFn); 10 | const [tryCount, setTryCount] = useState(0); 11 | 12 | const increment = useCallback(() => { 13 | setTryCount((v) => v + 1); 14 | }, [setTryCount]); 15 | 16 | const timer: MutableRefObject | undefined> = useRef(undefined); 17 | 18 | const start = useCallback( 19 | (...args: Args) => { 20 | timer.current = setTimeout(async function tick() { 21 | await load(...args); 22 | timer.current = setTimeout(tick, interval); 23 | }, interval); 24 | }, 25 | [load, interval, timer], 26 | ); 27 | 28 | useEffect(() => { 29 | if (result.isLoading) { 30 | increment(); 31 | } 32 | }, [result, increment]); 33 | 34 | const stop = useCallback(() => { 35 | clearTimeout(timer.current as NodeJS.Timer); 36 | }, [timer]); 37 | 38 | useEffect(() => { 39 | return () => { 40 | clearTimeout(timer.current as NodeJS.Timer); 41 | timer.current = undefined; 42 | }; 43 | }, [timer]); 44 | 45 | return [result, start, stop, load, reset, tryCount]; 46 | }; 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es6", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "declaration": true, 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "lib": ["DOM", "ES5", "ES2015", "ES2015.Promise"] 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist", "src/**/*.test.tsx", "src/**/*.test.ts"] 19 | } 20 | --------------------------------------------------------------------------------