diff --git a/docs/src/pages/_meta.json b/docs/src/pages/_meta.json index e879fcad..d10c87ed 100644 --- a/docs/src/pages/_meta.json +++ b/docs/src/pages/_meta.json @@ -38,6 +38,7 @@ "type": "separator", "title": "Hooks" }, + "use-abortable-effect": {}, "use-clipboard": {}, "use-composition-input": {}, "use-debounced-state": {}, diff --git a/docs/src/pages/best-practice.mdx b/docs/src/pages/best-practice.mdx index af82ed96..24f8fb5f 100644 --- a/docs/src/pages/best-practice.mdx +++ b/docs/src/pages/best-practice.mdx @@ -47,7 +47,7 @@ export { useSidebarActive, useSetSidebarActive }; ``` - + ```tsx filename="src/App.tsx" copy import { useIntersection } from 'foxact/use-intersection'; @@ -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 { @@ -155,5 +156,3 @@ const ExampleComponent = ({ dataKey }: ExampleComponentProps) => { │ Request data 2 ────► data2 response (setData(data2)) │ | isCancelled: false | isCancelled: false, setData(data2) ``` - - diff --git a/docs/src/pages/use-abortable-effect.mdx b/docs/src/pages/use-abortable-effect.mdx new file mode 100644 index 00000000..4c228abb --- /dev/null +++ b/docs/src/pages/use-abortable-effect.mdx @@ -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'; +``` diff --git a/package-lock.json b/package-lock.json index c852de6b..53919b75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "foxact", - "version": "0.2.30", + "version": "0.2.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "foxact", - "version": "0.2.30", + "version": "0.2.32", "license": "MIT", "dependencies": { "client-only": "^0.0.1", diff --git a/src/use-abortable-effect/index.ts b/src/use-abortable-effect/index.ts new file mode 100644 index 00000000..266df3d4 --- /dev/null +++ b/src/use-abortable-effect/index.ts @@ -0,0 +1,15 @@ +import 'client-only'; +import { type EffectCallback, useEffect as useEffectFromReact, type DependencyList } from 'react'; + +export const useAbortableEffect = (callback: (signal: AbortSignal) => ReturnType, deps: DependencyList) => { + useEffectFromReact(() => { + const controller = new AbortController(); + const signal = controller.signal; + const f = callback(signal); + return () => { + controller.abort(); + f?.(); + }; + }, deps); +}; +export const useEffect = useAbortableEffect;