diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..70c6dff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + pull_request: + branches: + - main + - develop + - 'release/v*.*.*' + push: + branches: + - main + - develop + - 'release/v*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Run checks + run: npm run verify + + - name: Build project + run: npm run build diff --git a/package-lock.json b/package-lock.json index 37ed0dc..4cdce54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "prettier": "^3.7.4", "react": "^17.0.2", "rimraf": "^6.1.2", - "saborter": "^1.4.2", + "saborter": "^2.0.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", "vite": "^7.3.0", @@ -36,7 +36,7 @@ }, "peerDependencies": { "react": ">=17.0.0 <=20.0.0", - "saborter": "^1.4.2" + "saborter": "^2.0.1" } }, "node_modules/@babel/code-frame": { @@ -6883,9 +6883,9 @@ } }, "node_modules/saborter": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/saborter/-/saborter-1.4.2.tgz", - "integrity": "sha512-+z+ixvBB8cHfmh19hjMVsHu+fKzhYjrFw/RwNkXUtwLkWIm2r7vxlNetzWQlDUUZPakG6lZOKsHjbeosmT9pIA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/saborter/-/saborter-2.0.1.tgz", + "integrity": "sha512-S8n1PcieQP+yPDyIl/PnMUOOF7pM5x+VhO/JDEoGsVmq8YHsCSMsY8dIWuD6uH22n4NxTg0RfUhyn5+N5LxwkA==", "dev": true, "license": "MIT" }, diff --git a/readme.md b/readme.md index 556f845..9eea17b 100644 --- a/readme.md +++ b/readme.md @@ -135,6 +135,63 @@ const { requestState } = useAborter(); console.log(requestState); // 'cancelled' / 'pending' / 'fulfilled' / 'rejected' / 'aborted' ``` +### `useReusableAborter` + +#### Props + +```typescript +// The type can be found in `saborter/types` +const reusableAborter = new useReusableAborter(props?: ReusableAborterProps); +``` + +#### Props Parameters + +| Parameter | Type | Description | Required | +| --------- | ---------------------- | ------------------------------------- | -------- | +| `props` | `ReusableAborterProps` | ReusableAborter configuration options | No | + +**ReusableAborterProps:** + +```typescript +{ + /** + * Determines which listeners are carried over when the abort signal is reset. + * - If `true`, all listeners (both `onabort` and event listeners) are preserved. + * - If `false`, no listeners are preserved. + * - If an object, specific listener types can be enabled/disabled individually. + */ + attractListeners?: boolean | AttractListeners; +} +``` + +#### Properties + +`signal: AbortSignal` + +Returns the `AbortSignal` associated with the current controller. + +```javascript +const reusableAborter = useReusableAborter(); + +// Using signal in the request +fetch('/api/data', { + signal: reusableAborter.signal +}); +``` + +#### Methods + +`abort(reason?): void` + +**Parameters:** + +- `reason?: any` - the reason for aborting the request. + +Immediately cancels the currently executing request. + +> [!NOTE] +> Can be called multiple times. Each call will restore the `signal`, and the `aborted` property will always be `false`. + ## 🎯 Usage Examples ### Basic Usage @@ -172,7 +229,7 @@ const Component = () => { }; ``` -### The `AbortError` `initiator` changed while unmounting the component. +### The `AbortError` `initiator` changed while unmounting the component ```javascript import { AbortError } from 'saborter'; @@ -193,6 +250,39 @@ const Component = () => { }; ``` +### Using `useReusableAborter` + +```typescript +const aborter = new useReusableAborter(); + +// Get the current signal +const signal = aborter.signal; + +// Attach listeners +signal.addEventListener('abort', () => console.log('Listener 1')); +signal.addEventListener('abort', () => console.log('Listener 2'), { once: true }); // won't be recovered + +// Set onabort handler +signal.onabort = () => console.log('Onabort handler'); + +// First abort +aborter.abort('First reason'); +// Output: +// Listener 1 +// Listener 2 (once) +// Onabort handler + +// The signal is now a fresh one, but the non‑once listeners and onabort are reattached +signal.addEventListener('abort', () => console.log('Listener 3')); // new listener, will survive next abort + +// Second abort +aborter.abort('Second reason'); +// Output: +// Listener 1 +// Onabort handler +// Listener 3 +``` + ## 📋 License MIT License - see [LICENSE](./LICENSE) for details. diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 135fb70..8ca50fb 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export * from './use-aborter'; +export * from './use-reusable-aborter'; diff --git a/src/hooks/use-reusable-aborter/index.ts b/src/hooks/use-reusable-aborter/index.ts new file mode 100644 index 0000000..915a57f --- /dev/null +++ b/src/hooks/use-reusable-aborter/index.ts @@ -0,0 +1 @@ +export * from './use-reusable-aborter'; diff --git a/src/hooks/use-reusable-aborter/use-reusable-aborter.ts b/src/hooks/use-reusable-aborter/use-reusable-aborter.ts new file mode 100644 index 0000000..6678684 --- /dev/null +++ b/src/hooks/use-reusable-aborter/use-reusable-aborter.ts @@ -0,0 +1,9 @@ +import { useRef } from 'react'; +import { ReusableAborter } from 'saborter'; +import { ReusableAborterProps } from 'saborter/types'; + +export const useReusableAborter = (props: ReusableAborterProps = {}) => { + const reusableAborter = useRef(new ReusableAborter(props)); + + return reusableAborter.current; +};