Skip to content

Commit

Permalink
Add Abortable (#3)
Browse files Browse the repository at this point in the history
* basic attempt

* fix all types

* split files

* convert states to statuses

* integrate linter

* lint fixes

* extract types

* add test, coverage

* lint fixes

* lint fixes

* change coverage in CI

* add build

* update example

* add esbuild and minification

* add documentation
  • Loading branch information
eskawl committed Nov 21, 2023
1 parent 0e417f9 commit 0f83ab0
Show file tree
Hide file tree
Showing 37 changed files with 1,695 additions and 294 deletions.
42 changes: 42 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"prettier",
],
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint"],
rules: {
// Disable base config that may casuse erros
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error", // or "error"
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
ignorePatterns: ["dist/**/*.*", "jest.config.js"],
};
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
run: yarn install --frozen-lockfile

- name: Test
run: yarn test --ci --coverage
run: yarn coverage --ci

- name: Coveralls
uses: coverallsapp/github-action@master
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
.github
188 changes: 144 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
## React Use Async Fn

React hook for managing the state of an async function
React hook for managing the state of async function and abortable async functions

### Features
- Execute and track not only API requests but any async functions.
- Abort async functions. Can be used to abort fetch API calls.
- Simple API to `await` async function to get its result while the hook manages the state.
- Callbacks for notifying success and error

### Installing

Expand All @@ -17,22 +23,110 @@ yarn add react-use-async-fn
```

### Importing

```js
const { useAsync, STATES } = require('react-use-async-fn');
const { useAsync, STATES } = require("react-use-async-fn");
```

or in ES6

```js
import { useAsync, STATES } from 'react-use-async-fn';
import { useAsync, STATES } from "react-use-async-fn";
```


### Usage:

#### Basic example:

```js
const [{ data, error, status}, trigger] = useAsync({
fn: getData,
});
```

You can provide your async function through the `fn` prop.
The hook returns the state of the async function and a `trigger`.
You can run you async function by calling the `trigger`.
Any arguments provided to the trigger will be provided to your function.

If needed you can even `await` your function by using `await` on trigger.

```js
const onClick = async () => {
const result = await trigger(2);
console.log({ result });
};
```

### API

#### useAsync

This hook manages the state of execution of an async function

**props:**
`fn`: Async function to track and execute.
`onDone`: (Optional) Callback function called when the `fn` is ran successfully. It will be called with the result and the args provided to the `fn`.
`onError`: (Optional) Callback function called when the `fn`failed with an error. It will be called with the error and the args provided to the `fn`.

**returns:**
Array of state, trigger.
`[state, trigger]`

`state.data`: The return value of the `fn`. Initially `null`.
`state.status`: [Status](#STATUSES) of the function. One of [`INITIAL`, `WORKING`, `DONE`, `FAILED`]
`state.error`: The error thrown by the `fn`. Initially `null`.
`state.isLoading`: boolean. `true` if `state.status` is `STATUS.WORKING`.
`trigger`: Function to call the provided `fn`. All arguments are forwarded to the `fn`. You can `await` the trigger to get the output of `fn`.


#### useAbortableAsync(props)

This function manages the state of an async function which can be aborted.

The `fn` prop requires the last argument to be of `AbortableLifecycle` to use this hook.

**props**

`fn`: `(...any[], { abortSignal: AbortSignal }) => Promise<any>` Async function to track and execute.

Other props are same as [useAsync](#useAsync).


**returns**

Array of state, actions
`[state, { trigger, abort }]`

`state`: same as `[useAsync](#useAsync)`.
`trigger`: function which takes all arguments for `fn`, execpt the last argument [AbortableLifecycle](#AbortableLifecycle). The `AbortableLifecycle` argument is automatically injected by the hook.
`abort`: function to abort the request.


#### STATUSES

Enum of statuses

`INITIAL`: The status before triggering
`WORKING`: The status after triggering but before completion
`DONE`: The status when completed successfully
`FAILED`: The status when failed

#### AbortableLifecycle
The last argument of an `AbortableAsyncFn`. It has the shape of
```
{
abortSignal: AbortSignal,
}
```

### Examples:

#### useAsync:

```js
import { useCallback } from 'react'
import { useAsync, STATES } from 'react-use-async-fn';
import { useAsync, STATUSES } from 'react-use-async-fn';

const sleep = () => new Promise(r => setTimeout(r, 5000))

Expand All @@ -59,50 +153,56 @@ function App() {
)
}

export default App

```

You can provide your async function through the `fn` prop.
The hook returns the state of the async function and a `trigger`.
You can run you async function by calling the `trigger`.
Any arguments provided to the trigger will be provided to your function.

If needed you can even `await` your function by using `await` on trigger.
#### useAbortableAsync

```js
const onClick = async () => {
const result = await trigger(2);
console.log({ result })
import { useCallback } from 'react'
import { AbortableLifecycle, STATUSES, useAbortableAsync } from 'react-use-async-fn';

function abortbleSleep(ms=5000, {abortSignal}: AbortableLifecycle): Promise<string>{
return new Promise((resolve, reject) => {
console.log("Promise Started");

let timeout: ReturnType<typeof setTimeout>;

const abortHandler = () => {
clearTimeout(timeout);
reject(new Error("Aborted"));
}

// start async operation
timeout = setTimeout(() => {
resolve("Promise Resolved");
abortSignal?.removeEventListener("abort", abortHandler);
}, ms);

abortSignal?.addEventListener("abort", abortHandler);
});
}
```

### API

useAsync(props)

**props:**
`fn`: Async function to track and execute.
`onDone`: (Optional) Callback function called when the `fn` is ran successfully. It will be called with the result and the args provided to the `fn`.
`onError`: (Optional) Callback function called when the `fn`failed with an error. It will be called with the error and the args provided to the `fn`.


**returns:**
Array of state, trigger.
`[state, trigger]`

`state.data`: The return value of the `fn`. Initially `null`.
`state.status`: `STATUS` of the function. One of [`INITIAL`, `WORKING`, `DONE`, `FAILED`]
`state.error`: The error thrown by the `fn`. Initially `null`.
`state.isLoading`: boolean. `true` if `state.status` is `STATUS.WORKING`.
`trigger`: Function to call the provided `fn`. All arguments are forwarded to the `fn`.
function App() {
const [{ data, error, status }, {trigger, abort}] = useAbortableAsync({
fn: abortbleSleep,
});

**STATUS:**
```js
STATES = {
INITIAL: "INITIAL",
WORKING: "WORKING",
DONE: "DONE",
FAILED: "FAILED",
};
return (
<div>
<h2>Abortable</h2>
<h3>data</h3>
<p>{data}</p>
<h3>status</h3>
<p>{status}</p>
<h3>error</h3>
<p>{(error as Error)?.message}</p>
<p>
<button onClick={() => trigger(5000)}>Trigger with 5000 ms</button>
</p>
<p>
<button disabled={status != STATUSES.WORKING} onClick={() => abort()}>Abort</button>
</p>
</div>
)
}
```
5 changes: 5 additions & 0 deletions dist/abortable.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AbortableAsyncAbort, AbortableAsyncTrigger, AsyncState, BasicAsyncFn, useAsyncArgs } from "./types";
export declare const useAbortableAsync: <F extends BasicAsyncFn>({ fn, onDone, onError, }: useAsyncArgs<F>) => [AsyncState<F>, {
trigger: AbortableAsyncTrigger<F>;
abort: AbortableAsyncAbort;
}];
89 changes: 89 additions & 0 deletions dist/abortable.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dist/abortable.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions dist/async.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { AsyncState, AsyncTrigger, BasicAsyncFn, useAsyncArgs } from "./types";
export declare const noOp: () => undefined;
export declare const useAsync: <F extends BasicAsyncFn>({ fn, onDone, onError, }: useAsyncArgs<F>) => [AsyncState<F>, AsyncTrigger<F>];

0 comments on commit 0f83ab0

Please sign in to comment.