{"id":16837302,"url":"https://github.com/janpaepke/reversible-effect","last_synced_at":"2026-05-01T10:31:18.674Z","repository":{"id":57688874,"uuid":"493316011","full_name":"janpaepke/reversible-effect","owner":"janpaepke","description":"A collection of typed utility functions returning a callback to reverse their effect.","archived":false,"fork":false,"pushed_at":"2026-02-13T16:16:40.000Z","size":484,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-02-14T00:36:53.132Z","etag":null,"topics":["effect","effects","javascript","typescript","utilities","utility","utility-library"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/janpaepke.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-05-17T15:45:11.000Z","updated_at":"2026-02-13T16:16:43.000Z","dependencies_parsed_at":"2024-10-13T12:16:54.667Z","dependency_job_id":"a8563afa-a30c-4914-b7e1-2c4e2edeb014","html_url":"https://github.com/janpaepke/reversible-effect","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/janpaepke/reversible-effect","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/janpaepke%2Freversible-effect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/janpaepke%2Freversible-effect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/janpaepke%2Freversible-effect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/janpaepke%2Freversible-effect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/janpaepke","download_url":"https://codeload.github.com/janpaepke/reversible-effect/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/janpaepke%2Freversible-effect/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32494270,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-30T13:12:12.517Z","status":"online","status_checked_at":"2026-05-01T02:00:05.856Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["effect","effects","javascript","typescript","utilities","utility","utility-library"],"created_at":"2024-10-13T12:16:52.633Z","updated_at":"2026-05-01T10:31:18.661Z","avatar_url":"https://github.com/janpaepke.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# reversible effect ↩️\n\n\u003ch3\u003eSelf-Cleanup for JavaScript Side-Effects\u003c/h3\u003e\n\n**A zero dependency collection of typed utility functions returning a callback to reverse their effect.**\n\n[\u003cimg alt=\"current version\" src=\"https://badgen.net/npm/v/reversible-effect\" /\u003e](https://www.npmjs.com/package/reversible-effect)\n[\u003cimg alt=\"no dependencies\" src=\"https://badgen.net/bundlephobia/dependency-count/reversible-effect?label=dependencies\u0026color=green\" /\u003e](https://bundlephobia.com/package/reversible-effect)\n[\u003cimg alt=\"tree shaking support\" src=\"https://badgen.net/bundlephobia/tree-shaking/reversible-effect\" /\u003e](https://bundlephobia.com/package/reversible-effect)\n[\u003cimg alt=\"tests\" src=\"https://badgen.net/github/checks/janpaepke/reversible-effect\" /\u003e](https://github.com/janpaepke/reversible-effect/actions)\n[\u003cimg alt=\"types included\" src=\"https://badgen.net/npm/types/reversible-effect\" /\u003e](https://www.npmjs.com/package/reversible-effect)\n[\u003cimg alt=\"MIT license\" src=\"https://badgen.net/npm/license/reversible-effect\" /\u003e](./LICENSE.md)\n[\u003cimg alt=\"gzip bundle size\" src=\"https://badgen.net/bundlephobia/minzip/reversible-effect?label=size\" /\u003e](https://bundlephobia.com/package/reversible-effect)\n\nThis package includes reversible implementations of:\n\n- [setTimeout](#setreversibletimeout)\n- [setInterval](#setreversibleinterval)\n- [addEventListener](#addreversibleeventlistener)\n- [requestAnimationFrame](#requestreversibleanimationframe)\n- [requestIdleCallback](#requestreversibleidlecallback)\n\n\u003chr /\u003e\n\nHey you! Yes, **you**!  \nAre you tired of writing this?\n\n```ts\nuseEffect(() =\u003e {\n\tconst myCallback = () =\u003e {\n\t\tconsole.log('window resized');\n\t};\n\twindow.addEventListener('resize', myCallback);\n\treturn () =\u003e {\n\t\twindow.removeEventListener('resize', myCallback);\n\t};\n}, []);\n```\n\nHow about writing this instead?\n\n\u003c!-- prettier-ignore-start --\u003e\n```ts\nuseEffect(\n    () =\u003e addReversibleEventListener(window, 'resize', () =\u003e {\n        console.log('window resized');\n    }), []\n);\n```\n\u003c!-- prettier-ignore-end --\u003e\n\nLet's find out [how](#documentation) or [why](#motivation).\n\n\u003chr /\u003e\n\n\u003ch2\u003eContent\u003c/h2\u003e\n\n\u003c!-- prettier-ignore-start --\u003e\n\n\u003c!-- toc --\u003e\n\n- [Installation](#installation)\n- [Motivation](#motivation)\n- [Documentation](#documentation)\n  - [Basic Usage](#basic-usage)\n  - [React's `useEffect`](#reacts-useeffect)\n  - [Available functions](#available-functions)\n    - [`addReversibleEventListener`](#addreversibleeventlistener)\n    - [`setReversibleTimeout`](#setreversibletimeout)\n    - [`setReversibleInterval`](#setreversibleinterval)\n    - [`requestReversibleAnimationFrame`](#requestreversibleanimationframe)\n    - [`requestReversibleIdleCallback`](#requestreversibleidlecallback)\n- [Contributing](#contributing)\n- [Support](#support)\n\n\u003c!-- tocstop --\u003e\n\n\u003c!-- prettier-ignore-end --\u003e\n\n## Installation\n\nusing npm:\n\n```sh\nnpm install reversible-effect\n```\n\nusing yarn:\n\n```sh\nyarn add reversible-effect\n```\n\nJust import the required function from the package, here's an example for `addReversibleEventListener`:\n\n```ts\nimport { addReversibleEventListener } from 'reversible-effect';\n```\n\nSee [below](#documentation) for available functions and how to use them.\n\n## Motivation\n\nFor a detailed explanation, check out [this Medium article](https://medium.com/@janpaepke/clean-up-after-yourself-e0eaa283e641), but here's the gist of it:\n\nFunctions like `setTimeout` or `addEventListener` come with an accompanying function to reverse their effect, like `clearTimeout` or `removeEventListener`.\n\nTo be able to use them, you need to keep track of which effect you're trying to reverse, so you can call the appropriate function. To make matters worse, these functions require different parameters.\n\nYou also need to keep track of all references to identify what it is you want to cancel (a `timeoutId` for `clearTimeOut` or the `eventName` as well as the `callback` for `removeEventListener`).\n\nThis package and the functions it provides aim to improve this:\n\n- **Unify the interface**:  \n  Doesn't matter what type of effect – just call function to reverse it.\n- **Simplify the implementation**:  \n  No need to keep track of timeout IDs or callback references.\n- **Provide type security**:  \n  When using typescript the respective function parameters should mirror the behaviour of their originals.\n\n## Documentation\n\n### Basic Usage\n\nEvery function in this package returns a callback, which serves as its cleanup.\n\nThis allows you to do this:\n\n```ts\n// create an interval\nconst cancelInterval = setReversibleInterval(() =\u003e console.log('called'), 1000);\n// cancel it\ncancelInterval();\n```\n\nNote that you don't have to keep track of a `timeoutID`, you can pass around a reference to the cleanup function to wherever you need it.  \nYou can then call it without needing to know that it is an interval you're cancelling.  \nIt would work just the same for an event:\n\n```ts\n// add an event listener\nconst remove = addReversibleEventListener(window, 'click', () =\u003e console.log('clicked'));\n// remove it\nremove();\n```\n\n### React's `useEffect`\n\nA common use case of these functions is within a react's `useEffect` hook, which conveniently expects you to return function to reverse the effect.  \nAs every function returns its own cleanup, you can return the reversible function from the hook directly.\n\nThis example will add the event listener, whenever the component mounts and remove it, when it unmounts:\n\n```ts\nuseEffect(() =\u003e addReversibleEventListener(window, 'click', e =\u003e console.log(e.clientX, e.clientY)), []);\n```\n\nHere we start an interval, when the component mounts and stop it, when it unmounts:\n\n```ts\nuseEffect(() =\u003e setReversibleInterval(() =\u003e setCount(count =\u003e count + 1), 500), []);\n```\n\n**Note**: If you add dependencies to the `useEffect` hook here, the timer will reset any time a dependency changes.  \nSee [here](https://overreacted.io/making-setinterval-declarative-with-react-hooks/) to learn why and how to get around this.\n\nIf you have multiple effects, you could either define multiple `useEffect` hooks or use them like this:\n\n```ts\nuseEffect(() =\u003e {\n\tconst cancelTimeout = setReversibleTimeout(() =\u003e {}, 1000);\n\tconst removeEventListener = addReversibleEventListener(window, 'click', () =\u003e {});\n\treturn () =\u003e {\n\t\tcancelTimeout();\n\t\tremoveEventListener();\n\t};\n}, []);\n```\n\n### Available functions\n\n#### `addReversibleEventListener`\n\nReversible version of `object.addEventListener`. [→ docs for original](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)\n\nThis function has two overloads, which switch based on the provided `target`. If `target` is an object we know the supported event types for, typescript will both limit `type` to supported types and provide the correct event type to the `callback`.\n\n```ts\nfunction addReversibleEventListener(\n\ttarget: EventTargetWithKnownEvents,\n\ttype: AvailableEventsForThisTarget, // string literal of available event listeners\n\tlistener: (e: SpecificEvent) =\u003e void, // Callback with Specific Event based on type\n\toptions?: boolean | AddEventListenerOptions\n): () =\u003e void;\n```\n\nIf we can't determine the event type, it will fall back to a generic version:\n\n```ts\nfunction addReversibleEventListener(\n\ttarget: GenericTarget,\n\ttype: string,\n\tlistener: (e: Event) =\u003e void,\n\toptions?: boolean | AddEventListenerOptions\n): () =\u003e void;\n```\n\nNote that there is shared behaviour with `addEventListener`/`removeEventListener` when called with the same arguments:\n\n1. When adding the same listener multiple times, it will not be added more than once.\n2. A returned cleanup can cancel any listener that was added using the same arguments.\n\nSee below examples for details:\n\n```ts\nconst callback = () =\u003e console.log('hello');\nconst cancel1 = addReversibleEventListener(window, 'click', callback);\ncancel1();\nconst cancel2 = addReversibleEventListener(window, 'click', callback);\nconst cancel3 = addReversibleEventListener(window, 'click', callback);\n//              ⬆ Since the passed arguments are the same, the callback will only be called once, when the event is fired.\ncancel1();\n// ⬆ Even though it has been used before this will remove the listener. It is functionally equivalent to `cancel2` and `cancel3`.\n```\n\nIf you specifically want to add a listener multiple times or make sure a cleanup function only concerns the listener it created, the solution is the same as with the originals – make the callback is referentially unique:\n\n```ts\nconst cancel1 = addReversibleEventListener(window, 'click', e =\u003e callback(e));\nconst cancel2 = addReversibleEventListener(window, 'click', e =\u003e callback(e));\n//              ⬆ The passed parameter is referentially unique, so the callback is executed twice, when the event is fired.\ncancel1();\n// ⬆ This will only ever remove the first listener, the second still fires, until `cancel2()` is called.\n```\n\n#### `setReversibleTimeout`\n\nReversible version of `[window.]setTimeout`. [→ docs for original](https://developer.mozilla.org/docs/Web/API/setTimeout)\n\n```ts\nfunction setReversibleTimeout(\n\tcallback: (...args: any[]) =\u003e void, // function to be executed after `delay`\n\tdelay?: number = 0, // delay for timeout\n\t...args: any[] // optional args to be passed into `callback`\n): () =\u003e void;\n```\n\n#### `setReversibleInterval`\n\nReversible version of `[window.]setInterval`. [→ docs for original](https://developer.mozilla.org/docs/Web/API/setInterval)\n\n```ts\nfunction setReversibleInterval(\n\tcallback: (...args: any[]) =\u003e void, // function to be executed every `delay`\n\tdelay?: number = 0, // delay between executions\n\t...args: any[] // optional args to be passed into `callback`\n): () =\u003e void;\n```\n\n#### `requestReversibleAnimationFrame`\n\nReversible version of `window.requestAnimationFrame`. [→ docs for original](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)\n\n```ts\nfunction requestReversibleAnimationFrame(\n\tcallback: (time: DOMHighResTimeStamp) =\u003e void // function to be executed on next repaint\n): () =\u003e void;\n```\n\n#### `requestReversibleIdleCallback`\n\nReversible version of `window.requestIdleCallback`. [→ docs for original](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)\n\n```ts\nfunction requestReversibleIdleCallback(\n\tcallback: (deadline: IdleDeadline) =\u003e void, // function to be called during idle periods\n\toptions?: { timeout?: number } // optional timeout to ensure execution\n): () =\u003e void;\n```\n\n## Contributing\n\nImprovements or additions are most welcome!\n\nThis package uses `npm` as its package manager.\n\nFork / Clone the repo, run `npm install`, then `npm start` to build in watch mode.\n\nRun `npm run test:watch` to run the tests, don't forget to add new tests, if you add functionality.\n\nCreate a PR, describing your change.\n\n## Support\n\nFound a bug or need help? [Add a new issue](https://github.com/janpaepke/reversible-effect/issues).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjanpaepke%2Freversible-effect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjanpaepke%2Freversible-effect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjanpaepke%2Freversible-effect/lists"}