Skip to content

Commit

Permalink
feat: add useAbortableEffect (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jack-Works authored Mar 8, 2024
1 parent 6a51eb3 commit f4f4b58
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/src/pages/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"type": "separator",
"title": "Hooks"
},
"use-abortable-effect": {},
"use-clipboard": {},
"use-composition-input": {},
"use-debounced-state": {},
Expand Down
7 changes: 3 additions & 4 deletions docs/src/pages/best-practice.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export {
useSidebarActive, useSetSidebarActive
};
```

```tsx filename="src/App.tsx" copy
import { useIntersection } from 'foxact/use-intersection';

Expand Down Expand Up @@ -124,7 +124,8 @@ const ExampleComponent = ({ dataKey }: ExampleComponentProps) => {

Here, although the request for `data1` happened before `data2`, the response for `data2` is received before `data1`. And `useIsMountedRef` doesn't help with that.

To properly avoid `setData(data1)` from being called, the correct pattern is:
To properly avoid `setData(data1)` from being called, the correct pattern is described below.
You can also use [`useAbortableEffect`](./use-abortable-effect) instead.

```tsx
interface ExampleComponentProps {
Expand Down Expand Up @@ -155,5 +156,3 @@ const ExampleComponent = ({ dataKey }: ExampleComponentProps) => {
│ Request data 2 ────► data2 response (setData(data2)) │
| isCancelled: false | isCancelled: false, setData(data2)
```


67 changes: 67 additions & 0 deletions docs/src/pages/use-abortable-effect.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: useAbortableEffect
---

# useAbortableEffect

`useEffect` that gives you an [AbortSignal](https://mdn.io/AbortSignal).

## Usage

```js
import { useAbortableEffect } from 'foxact/use-use-abortable-effect';

function Component() {
useAbortableEffect(signal => {
item.addEventListener('event', () => {
// ...
}, { signal })
}, [item]);
}
```

```js
// before
useEffect(() => {
let isCancelled = false;
someAsyncStuff().then(data => {
if (!isCancelled) {
setData(data);
}
});

return () => {
isCancelled = true;
};
}, [dataKey]);

// after
useAbortableEffect((signal) => {
someAsyncStuff().then(data => {
if (!signal.aborted) return
setData(data);
});
}, [dataKey]);
```

Note that [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) requires extra configuration in order to check dependency array for third-party hooks:

```json filename=".eslintrc.json" copy
{
"rules": {
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "useAbortableEffect"
}
]
}
}
```

But if you do not want to configure it, `foxact/use-abortable-effect` also provides another named export `useEffect` as an alias of `useAbortableEffect`:

```diff
- import { useEffect } from 'react';
+ import { useEffect } from 'foxact/use-abortable-effect';
```
4 changes: 2 additions & 2 deletions package-lock.json

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

15 changes: 15 additions & 0 deletions src/use-abortable-effect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'client-only';
import { type EffectCallback, useEffect as useEffectFromReact, type DependencyList } from 'react';

export const useAbortableEffect = (callback: (signal: AbortSignal) => ReturnType<EffectCallback>, deps: DependencyList) => {
useEffectFromReact(() => {
const controller = new AbortController();
const signal = controller.signal;
const f = callback(signal);
return () => {
controller.abort();
f?.();
};
}, deps);
};
export const useEffect = useAbortableEffect;

0 comments on commit f4f4b58

Please sign in to comment.