{"id":19592343,"url":"https://github.com/smikhalevski/react-executor","last_synced_at":"2025-07-05T02:33:54.100Z","repository":{"id":234952054,"uuid":"789707828","full_name":"smikhalevski/react-executor","owner":"smikhalevski","description":"🦖 Asynchronous task execution and state management for React.","archived":false,"fork":false,"pushed_at":"2025-07-04T10:08:12.000Z","size":1508,"stargazers_count":34,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-04T11:24:24.198Z","etag":null,"topics":["hook","react","state-management"],"latest_commit_sha":null,"homepage":"https://smikhalevski.github.io/react-executor/","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/smikhalevski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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}},"created_at":"2024-04-21T10:35:31.000Z","updated_at":"2025-07-04T10:08:16.000Z","dependencies_parsed_at":"2025-07-04T11:21:24.586Z","dependency_job_id":"fe6b8076-87e6-4971-850a-aca7c1ed9dd5","html_url":"https://github.com/smikhalevski/react-executor","commit_stats":null,"previous_names":["smikhalevski/react-executor"],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/smikhalevski/react-executor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Freact-executor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Freact-executor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Freact-executor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Freact-executor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smikhalevski","download_url":"https://codeload.github.com/smikhalevski/react-executor/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Freact-executor/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263502881,"owners_count":23476676,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["hook","react","state-management"],"created_at":"2024-11-11T08:34:43.215Z","updated_at":"2025-07-05T02:33:54.092Z","avatar_url":"https://github.com/smikhalevski.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"#readme\"\u003e\u003cpicture\u003e\n    \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"./assets/logo-dark.png\" /\u003e\n    \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"./assets/logo-light.png\" /\u003e\n    \u003cimg alt=\"React Executor\" src=\"./assets/logo-light.png\" width=\"700\" /\u003e\n  \u003c/picture\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n```sh\nnpm install --save-prod react-executor\n```\n\n🔥\u0026ensp;**Live examples**\n\n- [TODO app](https://codesandbox.io/p/sandbox/react-executor-example-ltflgy)\n- [Streaming SSR](https://codesandbox.io/p/devbox/react-executor-ssr-streaming-example-mwrmrs)\n- [Next.js integration](https://codesandbox.io/p/devbox/react-executor-next-example-whsj4v)\n\n🔰\u0026ensp;[**Introduction**](#introduction)\n\n- [Executor keys](#executor-keys)\n- [Execute a task](#execute-a-task)\n- [Abort a task](#abort-a-task)\n- [Replace a task](#replace-a-task)\n- [Wait for a task to complete](#wait-for-a-task-to-complete)\n- [Retry the latest task](#retry-the-latest-task)\n- [Settle an executor](#settle-an-executor)\n- [Clear an executor](#clear-an-executor)\n\n📢\u0026ensp;[**Events and lifecycle**](#events-and-lifecycle)\n\n- [Activate an executor](#activate-an-executor)\n- [Invalidate results](#invalidate-results)\n- [Detach an executor](#detach-an-executor)\n\n🔌\u0026ensp;[**Plugins**](#plugins)\n\n- [`abortDeactivated`](#abortdeactivated)\n- [`abortPendingAfter`](#abortpendingafter)\n- [`abortWhen`](#abortwhen)\n- [`bindAll`](#bindall)\n- [`detachDeactivated`](#detachdeactivated)\n- [`detachInactive`](#detachinactive)\n- [`invalidateAfter`](#invalidateafter)\n- [`invalidateByPeers`](#invalidatebypeers)\n- [`invalidatePeers`](#invalidatepeers)\n- [`rejectPendingAfter`](#rejectpendingafter)\n- [`resolveBy`](#resolveby)\n- [`retryActivated`](#retryactivated)\n- [`retryFulfilled`](#retryfulfilled)\n- [`retryInvalidated`](#retryinvalidated)\n- [`retryRejected`](#retryrejected)\n- [`retryWhen`](#retrywhen)\n- [`synchronizeStorage`](#synchronizestorage)\n\n⚛️\u0026ensp;[**React integration**](#react-integration)\n\n- [Suspense](#suspense)\n- [External executors](#external-executors)\n\n🚀\u0026ensp;[**Server-side rendering**](#server-side-rendering)\n\n- [Render to string](#render-to-string)\n- [Streaming SSR](#streaming-ssr)\n- [State serialization](#state-serialization)\n- [Content-Security-Policy support](#content-security-policy-support)\n- [Next.js integration](#nextjs-integration)\n\n⚙️\u0026ensp;[**Devtools**](#devtools)\n\n🍪\u0026ensp;**Cookbook**\n\n- [Optimistic updates](#optimistic-updates)\n- [Dependent tasks](#dependent-tasks)\n- [Pagination](#pagination)\n- [Infinite scroll](#infinite-scroll)\n- [Invalidate all executors](#invalidate-all-executors)\n- [Prefetching](#prefetching)\n- [Storage state versioning](#storage-state-versioning)\n- [Global loading indicator](#global-loading-indicator)\n\n# Introduction\n\nAn executor executes a task, stores the execution result, and provides access to it. Tasks are callbacks that return a\nvalue or throw an error.\n\nAn [`Executor`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html) is created and\nmanaged by\nan [`ExecutorManager`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html) which\ncontrols the executor lifecycle:\n\n```ts\nimport { ExecutorManager } from 'react-executor';\n\nconst manager = new ExecutorManager();\n\nconst rookyExecutor = manager.getOrCreate('rooky');\n// ⮕ Executor\u003cany\u003e\n```\n\nEach executor has a unique key in the scope of the manager. Here we created the new executor with the key `'rooky'`.\nManagers create a new executor when you call\n[`getOrCreate`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html#getOrCreate)\nwith a new key. Each consequent call with that key returns the same executor.\n\nIf you want to retrieve an existing executor by its key and don't want to create a new executor if it doesn't exist, use\n[`get`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html#get):\n\n```ts\nmanager.get('bobby');\n// ⮕ undefined\n\nmanager.get('rooky');\n// ⮕ Executor\u003cany\u003e\n```\n\nThe executor we created is unsettled, which means it neither stores a value, nor a task failure reason:\n\n```ts\nrookyExecutor.isSettled;\n// ⮕ false\n```\n\nAn executor can be created with an initial value:\n\n```ts\nconst bobbyExecutor = manager.getOrCreate('bobby', 42);\n\nbobbyExecutor.isSettled;\n// ⮕ true\n\n// The result stored in the executor is a value\nbobbyExecutor.isFulfilled;\n// ⮕ true\n\nbobbyExecutor.value;\n// ⮕ 42\n```\n\nAn initial value can be a task which is executed, a promise which the executor awaits, or any other value that instantly\nfulfills the executor. Read more in the [Execute a task](#execute-a-task) and in\nthe [Settle an executor](#settle-an-executor) sections.\n\nWhen an executor is created, you can provide an array of plugins:\n\n```ts\nimport retryRejected from 'react-executor/plugin/retryRejected';\n\nconst rookyExecutor = manager.getOrCreate('rooky', 42, [retryRejected()]);\n```\n\nPlugins can subscribe to [executor events](#events-and-lifecycle) or alter the executor instance. Read more about\nplugins in the [Plugins](#plugins) section.\n\n## Executor keys\n\nAnything can be an executor key: a string, a number, an object, etc. By default, keys are considered identical if\ntheir [`JSON`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON)-serialized form is\nidentical:\n\n```ts\nconst manager = new ExecutorManager();\n\nconst userExecutor = manager.getOrCreate(['user', 123]);\n\nmanager.get(['user', 123]);\n// ⮕ userExecutor\n```\n\nTo override, how keys are serialized\npass [`keySerializer`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.ExecutorManagerOptions.html#keySerializer)\noption to the `ExecutorManager` constructor. Key serializer is a function that receives the requested executor key and\nreturns its serialized form. The returned serialized key form can be anything, a string, or an object.\n\nIf you're using objects as executor keys, then you may want to enable stable serialization (when keys are sorted\nalphabetically during serialization). In this case use any library that supports stable JSON serialization:\n\n```ts\nimport { stringify } from 'json-marshal';\n\nconst manager = new ExecutorManager({\n  keySerializer: key =\u003e stringify(key, { isStable: true })\n});\n\nconst bobrExecutor = manager.getOrCreate({ id: 123, name: 'Woody' });\n// ⮕ Executor\u003cany\u003e\n\n// 🟡 Key properties are listed in a different order\nmanager.get({ name: 'Woody', id: 123 });\n// ⮕ bobrExecutor\n```\n\n\u003e [!TIP]\\\n\u003e With additional configuration, [json-marshal](https://github.com/smikhalevski/json-marshal#readme) can stringify and\n\u003e parse any data structure.\n\nIf you want to use object references as executor keys, provide an identity function as a serializer:\n\n```ts\nconst manager = new ExecutorManager({\n  keySerializer: key =\u003e key\n});\n\nconst bobrKey = { id: 123 };\n\nconst bobrExecutor = manager.getOrCreate(bobrKey);\n\n// The same executor is returned for the same key\nmanager.get(bobrKey);\n// ⮕ bobrExecutor\n\nconst anotherBobrExecutor = manager.getOrCreate({ id: 123 });\n\n// 🟡 Executors are different because different objects were used as keys\nbobrExecutor === anotherBobrExecutor;\n// ⮕ false\n```\n\n## Execute a task\n\nLet's execute a new task:\n\n```ts\nimport { ExecutorManager, ExecutorTask } from 'react-executor';\n\nconst manager = new ExecutorManager();\n\nconst rookyExecutor = manager.getOrCreate('rooky');\n\nconst helloTask: ExecutorTask = async (signal, executor) =\u003e 'Hello';\n\nconst helloPromise = rookyExecutor.execute(task);\n// ⮕ AbortablePromise\u003cany\u003e\n```\n\n`helloTask` receives an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) and\n`rookyExecutor` as arguments. The signal is aborted if the task is [aborted](#abort-a-task) or\n[replaced](#replace-a-task).\n\nWhile tasks can be synchronous or asynchronous, executors always handle them in an asynchronous fashion. The executor is\nmarked as [pending](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#isPending)\nimmediately after\n[`execute`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#execute) is called:\n\n```ts\n// The executor is waiting for the task to complete\nrookyExecutor.isPending;\n// ⮕ true\n```\n\n`helloPromise` is resolved when the task completes:\n\n```ts\nawait helloPromise;\n\n// The executor doesn't have a pending task anymore\nrookyExecutor.isPending;\n// ⮕ false\n\n// The result stored in the executor is a value\nrookyExecutor.isFulfilled;\n// ⮕ true\n\nrookyExecutor.value;\n// ⮕ 'Hello'\n```\n\nThe executor keeps track of\nthe [latest task](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#task) it\nhas executed:\n\n```ts\nrookyExecutor.task;\n// ⮕ helloTask\n```\n\nIf a task throws an error (or returns a promise that rejects with an error), then the promise returned from the\n`execute` is rejected:\n\n```ts\nconst ooopsPromise = rookyExecutor.execute(() =\u003e {\n  throw new Error('Ooops!');\n});\n// ⮕ Promise{\u003crejected\u003e}\n\nrookyExecutor.isPending;\n// ⮕ true\n```\n\nThe executor becomes rejected as well after `ooopsPromise` is settled:\n\n```ts\nrookyExecutor.isRejected;\n// ⮕ true\n\n// The reason of the task failure\nrookyExecutor.reason;\n// ⮕ Error('Ooops!')\n```\n\nExecutors always preserve the latest value and the latest reason. So even when the executor\n[`isPending`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#isPending), you can\naccess the previous value or failure reason. Use\n[`isFulfilled`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#isFulfilled) and\n[`isRejected`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#isRejected) to \ndetect with what result the executor has settled the last time. An executor cannot be both fulfilled and rejected at the\nsame time.\n\n```ts\n// Execute a new task\nconst byePromise = rookyExecutor.execute(() =\u003e 'Bye');\n\n// 1️⃣ The executor is waiting for the task to complete\nrookyExecutor.isPending;\n// ⮕ true\n\n// 2️⃣ The executor is still rejected after the previous task\nrookyExecutor.isRejected;\n// ⮕ true\n\nrookyExecutor.reason;\n// ⮕ Error('Ooops!')\n\n// 3️⃣ The executor still holds the latest value, but it isn't fulfilled\nrookyExecutor.isFulfilled;\n// ⮕ false\n\nrookyExecutor.value;\n// ⮕ 'Hello'\n```\n\nThe executor becomes fulfilled after `byePromise` settles:\n\n```ts\nawait byePromise;\n\nrookyExecutor.isFulfilled;\n// ⮕ true\n\nrookyExecutor.value;\n// ⮕ 'Bye'\n```\n\n## Abort a task\n\nThe promise returned by\nthe [`execute`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#execute)\nmethod is [abortable](https://smikhalevski.github.io/parallel-universe/classes/AbortablePromise.html) so\nthe task can be prematurely aborted. Results of the aborted task are discarded:\n\n```ts\nconst helloPromise = rookyExecutor.execute(async () =\u003e 'Hello');\n\nrookyExecutor.isPending;\n// ⮕ true\n\nhelloPromise.abort();\n\nrookyExecutor.isPending;\n// ⮕ false\n```\n\nIt isn't always convenient to keep the reference to the task execution promise, and you can abort the pending task by\naborting the whole executor:\n\n```ts\nrookyExecutor.abort();\n```\n\nIf there's no pending task, then aborting an executor is a no-op.\n\nWhen a task is aborted, the signal it received as an argument is aborted as well. Check\nthe [signal status](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/aborted) to ensure that computation\nshould be concluded.\n\nFor example, if you're fetching data from the server inside a task, you can pass signal as\na [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch#signal) option:\n\n```ts\nconst byeTask: ExecutorTask = async (signal, executor) =\u003e {\n  const response = await fetch('/bye', { signal });\n  \n  return response.json();\n};\n```\n\n## Replace a task\n\nIf a new task is executed while the pending task isn't completed yet, then pending task is aborted and its results are\ndiscarded:\n\n```ts\nexecutor.execute(async signal =\u003e 'Pluto');\n\nawait executor.execute(async signal =\u003e 'Mars');\n\nexecutor.value;\n// ⮕ 'Mars'\n```\n\n## Wait for a task to complete\n\nIn the [Execute a task](#execute-a-task) section we used a promise that is returned from\n[`Executor.execute`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#execute) to\nwait for a task execution to complete. While this approach allows to wait for a given task execution to settle, it is\nusually required to wait for an executor itself become settled. The main point here is that the executor remains\npending while multiple tasks [replace one another](#replace-a-task).\n\nLet's consider the scenario where a task is replaced with another task:\n\n```ts\nconst planetExecutor = manager.getOrCreate('planet');\n\n// The promise is resolved only when planetExecutor is settled\nconst planetPromise = planetExecutor.getOrAwait();\n\nconst marsPromise = planetExecutor.execute(async signal =\u003e 'Mars');\n\n// 🟡 marsPromise is aborted, because task was replaced\nconst venusPromise = planetExecutor.execute(async signal =\u003e 'Venus');\n\nawait planetPromise;\n// ⮕ 'Venus'\n```\n\nIn this example, `marsPromise` is aborted, and `planetPromise` is resolved only after executor itself is settled and\nnot pending anymore.\n\nHere's another example, where the executor waits to be settled:\n\n```ts\nconst printerExecutor = manager.getOrCreate('printer');\n\nprinterExecutor.getOrAwait().then(value =\u003e {\n  console.log(value);\n});\n\n// Prints \"Hello\" to console\nprinterExecutor.execute(() =\u003e 'Hello');\n```\n\n## Retry the latest task\n\nTo retry\nthe [latest task](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#task),\nuse [`retry`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#retry):\n\n```ts\nconst planets = ['Mars', 'Venus'];\n\nawait executor.execute(() =\u003e planets.shift());\n\nexecutor.retry();\n\nawait executor.getOrAwait();\n\nexecutor.value;\n// ⮕ 'Mars'\n```\n\nIf there's no latest task, or there's a pending task already, then calling `retry` is a no-op.\n\nIf you want to forcefully retry the latest task, then abort the executor first:\n\n```ts\nexecutor.abort();\nexecutor.retry();\n```\n\n## Settle an executor\n\nWhile tasks are always handled in an asynchronous fashion, there are cases when an executor should be settled\nsynchronously.\n\nExecutor can be synchronously fulfilled via\n[`resolve`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#resolve):\n\n```ts\nexecutor.resolve('Venus');\n\nexecutor.isFulfilled;\n// ⮕ true\n\nexecutor.value;\n// ⮕ 'Venus'\n```\n\nOr rejected\nvia [`reject`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#reject):\n\n```ts\nexecutor.reject(new Error('Ooops!'));\n\nexecutor.isRejected;\n// ⮕ true\n\nexecutor.reason;\n// ⮕ Error('Ooops!')\n```\n\nIf there is a pending task then invoking `resolve` or `reject` will [abort it](#abort-a-task).\n\nIf you pass a promise to `resolve`, then an executor would wait for it to settle and store the result:\n\n```ts\nconst planetPromise = Promise.resolve('Mars');\n\nexecutor.resolve(planetPromise);\n\n// The executor is waiting for the promise to settle\nexecutor.isPending;\n// ⮕ true\n\nawait executor.getOrAwait();\n\nexecutor.value;\n// ⮕ 'Mars'\n```\n\n## Clear an executor\n\nAfter the executor\nbecomes [settled](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#isSettled),\nit remains settled until it is cleared.\n\nYou can reset the executor back to its unsettled state\nusing [`clear`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#clear):\n\n```ts\nexecutor.clear();\n```\n\nClearing an executor removes the stored value and reason, but _doesn't_ affect the pending task execution and preserves\nthe [latest task](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#task) that\nwas executed.\n\n# Events and lifecycle\n\nExecutors publish various events when their state changes. To subscribe to executor events use the\n[`subscribe`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#subscribe) method:\n\n```ts\nconst manager = new ExecutorManager();\n\nconst rookyExecutor = manager.getOrCreate('rooky');\n\nconst unsubscribe = rookyExecutor.subscribe(event =\u003e {\n  if (event.type === 'fulfilled') {\n    // Handle the event here\n  }\n});\n\nunsubscribe();\n```\n\nYou can subscribe to the executor manager to receive events from all executors. For example, you can automatically retry\nany invalidated executor:\n\n```ts\nmanager.subscribe(event =\u003e {\n  if (event.type === 'invalidated') {\n    event.target.retry();\n  }\n});\n```\n\nBoth executors and managers may have multiple subscribers and each subscriber receives\n[events](https://smikhalevski.github.io/react-executor/interfaces/react_executor.ExecutorEvent.html) with following\ntypes:\n\n\u003cdl\u003e\n\u003cdt\u003eattached\u003c/dt\u003e\n\u003cdd\u003e\n\nThe executor was just created, plugins were applied to it, and it was attached to the manager. Read more about plugins\nin the [Plugins](#plugins) section.\n\n\u003c/dd\u003e\n\n\u003cdt\u003edetached\u003c/dt\u003e\n\u003cdd\u003e\n\nThe executor was just detached: it was removed from the manager and all of its subscribers were unsubscribed. Read more\nin the [Detach an executor](#detach-an-executor) section.\n\n\u003c/dd\u003e\n\n\u003cdt\u003eactivated\u003c/dt\u003e\n\u003cdd\u003e\n\nThe executor was inactive and became active. This means that there are consumers that observe the state of the executor.\nRead more in the [Activate an executor](#activate-an-executor) section.\n\n\u003c/dd\u003e\n\n\u003cdt\u003edeactivated\u003c/dt\u003e\n\u003cdd\u003e\n\nThe executor was active and became inactive. This means that there are no consumers that observe the state of the\nexecutor. Read more in the [Activate an executor](#activate-an-executor) section.\n\n\u003c/dd\u003e\n\n\u003cdt\u003epending\u003c/dt\u003e\n\u003cdd\u003e\n\nThe executor started [a task execution](#execute-a-task). You can find the latest task the executor handled in the\n[`Executor.task`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#task)\nproperty.\n\n\u003c/dd\u003e\n\n\u003cdt\u003efulfilled\u003c/dt\u003e\n\u003cdd\u003e\n\nThe executor was fulfilled with a value.\n\n\u003c/dd\u003e\n\n\u003cdt\u003erejected\u003c/dt\u003e\n\u003cdd\u003e\n\nThe executor was rejected with a reason.\n\n\u003c/dd\u003e\n\n\u003cdt\u003eaborted\u003c/dt\u003e\n\u003cdd\u003e\n\nThe [task was aborted](#abort-a-task).\n\nIf executor is\nstill [pending](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#isPending) when\nan `'aborted'` event is published then the currently pending task is being [replaced](#replace-a-task) with a new task.\n\nCalling [`Executor.execute`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#execute)\nwhen handling an abort event may lead to stack overflow. If you need to do this anyway, execute a new task from async\ncontext using [`queueMicrotask`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) or a similar API.\n\n\u003c/dd\u003e\n\n\u003cdt\u003ecleared\u003c/dt\u003e\n\u003cdd\u003e\u003cp\u003eThe executor was cleared and now isn't settled.\u003c/p\u003e\u003c/dd\u003e\n\n\u003cdt\u003einvalidated\u003c/dt\u003e\n\u003cdd\u003e\n\nResults stored in an executor were [invalidated](#invalidate-results).\n\n\u003c/dd\u003e\n\n\u003cdt\u003eannotated\u003c/dt\u003e\n\u003cdd\u003e\n\n[Annotations](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#annotations)\nassociated with the executor were patched.\n\n\u003c/dd\u003e\n\n\u003cdt\u003eplugin_configured\u003c/dt\u003e\n\u003cdd\u003e\n\nThe configuration of the plugin associated with the executor was updated.\n\n\u003c/dd\u003e\n\u003c/dl\u003e\n\n## Activate an executor\n\nExecutors have\nan [active](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#isActive) status that\ntells whether executor is actively used by a consumer.\n\n```ts\nconst deactivate = executor.activate();\n\nexecutor.isActive;\n// ⮕ true\n\ndeactivate();\n\nexecutor.isActive;\n// ⮕ false\n```\n\nIf there are multiple consumers and each of them invoke the `activate` method, then executor would remain active until\nall of them invoke their deactivate callbacks.\n\nBy default, marking an executor as active has no additional effect. Checking the executor active status in a plugin\nallows to skip or defer excessive updates and keep executor results up-to-date lazily. For example, consider a plugin\nthat [retries the latest task](#retry-the-latest-task) if an active executor becomes rejected:\n\n```ts\nconst retryPlugin: ExecutorPlugin = executor =\u003e {\n  executor.subscribe(event =\u003e {\n    switch (event.type) {\n\n      case 'rejected':\n      case 'activated': \n        if (executor.isActive \u0026\u0026 executor.isRejected) {\n          executor.retry();\n        }\n        break;\n    }\n  });\n};\n\nconst executor = manager.getOrCreate('rooky', heavyTask, [retryPlugin]);\n\nexecutor.activate();\n```\n\nNow an executor would automatically retry the `heavyTask` if it fails. Read more about plugins in\nthe [Plugins](#plugins) section.\n\n## Invalidate results\n\nInvalidate results stored in the executor:\n\n```ts\nexecutor.invalidate();\n\nexecutor.isInvalidated;\n// ⮕ true\n```\n\nAfter the executor is fulfilled, rejected, or cleared, it becomes valid:\n\n```ts\nexecutor.resolve('Okay');\n\nexecutor.isInvalidated;\n// ⮕ false\n```\n\nBy default, invalidating an executor has no effect except marking it\nas [invalidated](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#isInvalidated).\n\n## Detach an executor\n\nBy default, executors that a manager has created are preserved indefinitely and are always available though\n[`get`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html#get). This isn't\nalways optimal, and you may want to detach an executor when it isn't needed anymore.\nUse [`detach`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html#detach) in\nsuch case:\n\n```ts\nconst executor = manager.getOrCreate('test');\n\nmanager.detach(executor.key);\n// ⮕ true\n```\n\nAll executor subscribers are now unsubscribed, and executor is removed from the manager.\n\nIf an executor is still [active](#activate-an-executor) then it won't be detached.\n\n\u003e [!NOTE]\\\n\u003e Pending task isn't aborted if the executor is detached. Use [`abortDeactivated`](#abortdeactivated) plugin to abort\n\u003e the task of the deactivated executor.\n\n# Plugins\n\nPlugins are callbacks that are invoked only once when the executor is created by the manager. For example, you can\ncreate a plugin that aborts the pending task and [detaches an executor](#detach-an-executor) when it is\n[deactivated](#activate-an-executor):\n\n```ts\nconst detachPlugin: ExecutorPlugin = executor =\u003e {\n  executor.subscribe(event =\u003e {\n    if (event.type === 'deactivted') {\n      executor.abort();\n      executor.manager.detach(executor.key);\n    }\n  });\n};\n```\n\nTo apply a plugin, pass it to the\n[`ExecutorManager.getOrCreate`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html#getOrCreate)\nor to the [`useExecutor`](https://smikhalevski.github.io/react-executor/functions/react_executor.useExecutor.html) hook:\n\n```ts\nconst executor = manager.getOrCreate('test', undefined, [detachPlugin]);\n\nconst deactivate = executor.activate();\n\n// The executor is instantly detached by the plugin\ndeactivate();\n\nmanager.get('test');\n// ⮕ undefined\n```\n\nYou can define plugins that are applied to all executors that are created by a manager:\n\n```ts\nconst manager = new ExecutorManager({\n  plugins: [bindAll()]\n});\n\nconst { execute } = manager.getOrCreate('test');\n\n// Methods can be detached because bindAll plugin was applied\nexecute(heavyTask)\n```\n\n## `abortDeactivated`\n\n[Aborts the pending task](#abort-a-task) after the delay if the executor is deactivated.\n\n```ts\nimport abortDeactivated from 'react-executor/plugin/abortDeactivated';\n\nconst executor = useExecutor('test', heavyTask, [\n  abortDeactivated({ delay: 2_000 })\n]);\n\nconst deactivate = executor.activate();\n\n// Aborts heavyTask in 2 seconds\ndeactivate();\n```\n\nIf an executor is re-activated during this delay, the task won't be aborted. The executor must be activated at least\nonce for this plugin to have an effect.\n\n## `abortPendingAfter`\n\n[Aborts the pending task](#abort-a-task)\nwith [`TimeoutError`](https://developer.mozilla.org/en-US/docs/Web/API/DOMException#timeouterror) if the task execution\ntook longer then the given delay.\n\n```ts\nimport abortPendingAfter from 'react-executor/plugin/abortPendingAfter';\n\nconst executor = useExecutor('test', heavyTask, [\n  abortPendingAfter(10_000)\n]);\n```\n\n## `abortWhen`\n\n[Aborts the pending task](#abort-a-task) if the\n[observable](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Observable.html) emits `true`.\n\nFor example, abort the current task if the device is disconnected from the network for more then 5 seconds:\n\n```ts\nimport abortWhen from 'react-executor/plugin/abortWhen';\nimport navigatorOffline from 'react-executor/observable/navigatorOffline';\n\nconst executor = useExecutor('test', heavyTask, [\n  abortWhen(navigatorOffline, { delay: 5_000 })\n]);\n```\n\nIf a new task is passed to the\n[`Executor.execute`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#execute)\nmethod after the delay has run out then the task is instantly aborted.\n\nRead more about observables in the [`retryWhen`](#retrywhen) section.\n\n## `bindAll`\n\nBinds all executor methods to the instance.\n\n```ts\nimport bindAll from 'react-executor/plugin/bindAll';\n\n// Methods can now be detached from the executor instance\nconst { resolve } = useExecutor('test', 'Bye', [bindAll()]);\n\nresolve('Hello');\n```\n\nIt is handy to enable this plugin for all executors created by the execution manager:\n\n```ts\nimport { ExecutorManager } from 'react-executor';\nimport bindAll from 'react-executor/plugin/bindAll';\n\nconst manager = new ExecutorManager({\n  plugins: [bindAll()]\n});\n```\n\nProvide the manager so the `useExecutor` hook would employ it to create new executors:\n\n```tsx\n\u003cExecutorManagerProvider value={manager}\u003e\n  \u003cApp/\u003e\n\u003c/ExecutorManagerProvider\u003e\n```\n\n## `detachDeactivated`\n\n[Detaches the executor](#detach-an-executor) after the timeout if the executor is deactivated.\n\n```ts\nimport detachDeactivated from 'react-executor/plugin/detachDeactivated';\n\nconst executor = useExecutor('test', heavyTask, [\n  detachDeactivated({ delay: 2_000 })\n]);\n\nconst deactivate = executor.activate();\n\n// Executor is detached in 2 seconds\ndeactivate();\n```\n\nIf an executor is re-activated during this delay, the executor won't be detached.\n\nThis plugin doesn't abort the pending task when an executor is detached. Use [`abortDeactivated`](#abortdeactivated)\nto do the job:\n\n```ts\nimport abortDeactivated from 'react-executor/plugin/abortDeactivated';\nimport detachDeactivated from 'react-executor/plugin/detachDeactivated';\n\nconst executor = useExecutor('test', heavyTask, [\n  abortDeactivated({ delay: 2_000 }),\n  detachDeactivated({ delay: 2_000 })\n]);\n\nconst deactivate = executor.activate();\n\n// The heavyTask is aborted and the executor is detached in 2 seconds\ndeactivate();\n```\n\n## `detachInactive`\n\nDetach an executor if it wasn't activated during first 5 seconds after being created:\n\n```ts\nimport detachInactive from 'react-executor/plugin/detachInactive';\n\nconst executor = useExecutor('test', 42, [\n  detachInactive({ delayBeforeActivation: 5_000 })\n]);\n```\n\nDetach an executor if it was inactive for 5 seconds:\n\n```ts\nconst executor = useExecutor('test', 42, [\n  detachInactive({ delayAfterActivation: 5_000 })\n]);\n\nconst deactivate = executor.activate();\n\n// The executor is detached in 5 seconds\ndeactivate();\n```\n\n## `invalidateAfter`\n\nInvalidates the executor result after a delay.\n\n```ts\nimport invalidateAfter from 'react-executor/plugin/invalidateAfter';\n\nconst executor = useExecutor('test', 42, [invalidateAfter(2_000)]);\n\n// The executor is invalidated in 2 seconds\nexecutor.activate();\n```\n\nIf the executor is settled then the timeout is restarted.\n\n## `invalidateByPeers`\n\nInvalidates the executor result if another executor with a matching key is fulfilled or invalidated.\n\n```ts\nimport invalidateByPeers from 'react-executor/plugin/invalidateByPeers';\n\nconst cheeseExecutor = useExecutor('cheese', 'Burrata', [\n  invalidateByPeers(executor =\u003e executor.key === 'bread')\n]);\n\nconst breadExecutor = useExecutor('bread');\n\n// cheeseExecutor is invalidated\nbreadExecutor.resolve('Ciabatta');\n```\n\nProvide an array of executors as peers:\n\n```ts\nconst breadExecutor = useExecutor('bread');\n\nconst cheeseExecutor = useExecutor('cheese', 'Burrata', [\n  invalidateByPeers([breadExecutor])\n]);\n\n// cheeseExecutor is invalidated\nbreadExecutor.resolve('Ciabatta');\n```\n\n## `invalidatePeers`\n\nInvalidates peer executors with matching keys if the executor is fulfilled or invalidated.\n\n```ts\nimport invalidatePeers from 'react-executor/plugin/invalidatePeers';\n\nconst cheeseExecutor = useExecutor('cheese', 'Burrata', [\n  invalidatePeers(executor =\u003e executor.key === 'bread')\n]);\n\nconst breadExecutor = useExecutor('bread', 'Focaccia');\n\n// breadExecutor is invalidated\ncheeseExecutor.resolve('Mozzarella');\n```\n\nProvide an array of executors as peers:\n\n```ts\nconst breadExecutor = useExecutor('bread', 'Focaccia');\n\nconst cheeseExecutor = useExecutor('cheese', 'Burrata', [\n  invalidatePeers([breadExecutor])\n]);\n\n// breadExecutor is invalidated\ncheeseExecutor.resolve('Mozzarella');\n```\n\n## `rejectPendingAfter`\n\n[Aborts the pending task](#abort-a-task) and [rejects the executor](#settle-an-executor)\nwith [`TimeoutError`](https://developer.mozilla.org/en-US/docs/Web/API/DOMException#timeouterror) if the task execution\ntook longer then the given timeout.\n\n```ts\nimport rejectPendingAfter from 'react-executor/plugin/rejectPendingAfter';\n\nconst executor = useExecutor('test', heavyTask, [\n  rejectPendingAfter(10_000)\n]);\n```\n\n## `resolveBy`\n\n[Resolves the executor](#settle-an-executor) with values pushed by an\n[`Observable`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Observable.html).\n\n```ts\nimport { Observable } from 'react-executor';\nimport resolveBy from 'react-executor/plugin/resolveBy';\n\nconst observable: Observable\u003cstring\u003e = {\n  subscribe(listener) {\n    // Call the listener when value is changed\n    const timer = setTimeout(listener, 1_000, 'Venus');\n\n    return () =\u003e {\n      // Unsubscribe the listener\n      clearTimeout(timer);\n    };\n  }\n};\n\nconst executor = useExecutor('planet', 'Mars', [\n  resolveBy(observable)\n]);\n```\n\n[`PubSub`](https://smikhalevski.github.io/parallel-universe/classes/PubSub.html) can be used do decouple the lazy data source from the executor:\n\n```ts\nimport { PubSub } from 'parallel-universe';\n\nconst pubSub = new PubSub\u003cstring\u003e();\n\nconst executor = useExecutor('planet', 'Mars', [\n  resolveBy(pubSub)\n]);\n\npubSub.publish('Venus');\n\nexecutor.value;\n// ⮕ 'Venus'\n```\n\n## `retryActivated`\n\n[Retries the latest task](#retry-the-latest-task) if the executor is activated.\n\n```ts\nimport retryActivated from 'react-executor/plugin/retryActivated';\n\nconst executor = useExecutor('test', heavyTask, [retryActivated()]);\n\n// Retries the task\nexecutor.activate();\n```\n\nSet the minimum delay in milliseconds that should pass between the activation and the moment the executor was last\nsettled:\n\n```ts\nconst executor = useExecutor('test', heavyTask, [\n  retryActivated({ staleDelay: 5_000 })\n]);\n\n// Doesn't retry the task if 5 seconds didn't pass\nexecutor.activate();\n```\n\n## `retryFulfilled`\n\n[Retries the latest task](#retry-the-latest-task) after the execution was fulfilled.\n\n```ts\nimport retryFulfilled from 'react-executor/plugin/retryFulfilled';\n\nconst executor = useExecutor('test', heavyTask, [retryFulfilled()]);\n\nexecutor.activate();\n```\n\nIf the task fails, is aborted, or if an executor is deactivated then the plugin stops the retry process.\n\nWith the default configuration, the plugin would infinitely retry the task of an active executor with a 5-second delay\nbetween retries. This is effectively a decent polling strategy that kicks in only if someone is actually using an\nexecutor.\n\nSpecify the number of times the task should be re-executed if it succeeds:\n\n```ts\nretryFulfilled({ count: 3 })\n```\n\nSpecify the delay in milliseconds between retries:\n\n```ts\nretryFulfilled({ count: 3, delay: 5_000 });\n```\n\nProvide a function that returns the delay depending on the number of retries:\n\n```ts\nretryFulfilled({\n  count: 5,\n  delay: (index, executor) =\u003e 1000 * index\n});\n```\n\nBy default, `retryFulfilled` doesn't retry inactive executors. The executor is retried only after it becomes active.\n\nTo retry the latest task regardless of the executor activation status:\n\n```ts\nretryFulfilled({ isEager: true });\n```\n\n## `retryInvalidated`\n\nRetries the latest task of the active executor if it was invalidated.\n\n```ts\nimport retryInvalidated from 'react-executor/plugin/retryInvalidated';\n\nconst executor = useExecutor('test', 42, [retryInvalidated()]);\n\nexecutor.activate();\n```\n\nCombine this plugin with [`invalidateByPeers`](#invalidatebypeers) to automatically retry this executor if another\nexecutor on which it depends becomes invalid:\n\n```ts\nimport { ExecutorTask, useExecutor } from 'react-executor';\nimport invalidateByPeers from 'react-executor/plugin/invalidateByPeers';\n\nconst fetchCheese: ExecutorTask = async (signal, executor) =\u003e {\n  \n  // Wait for the breadExecutor to be created\n  const breadExecutor = await executor.manager.getOrAwait('bread');\n\n  // Wait for the breadExecutor to be settled\n  const bread = await breadExecutor.getOrAwait();\n  \n  // Choose the best cheese for this bread\n  return bread === 'Ciabatta' ? 'Mozzarella' : 'Burrata';\n};\n\nconst cheeseExecutor = useExecutor('cheese', fetchCheese, [\n  invalidateByPeers(executor =\u003e executor.key === 'bread'),\n  retryInvalidated(),\n]);\n\nconst breadExecutor = useExecutor('bread');\n\n// 🟡 cheeseExecutor is invalidated and re-fetches cheese\nbreadExecutor.resolve('Ciabatta');\n```\n\nRead more about [dependent tasks](#dependent-tasks).\n\nBy default, `retryInvalidated` doesn't retry inactive executors. The executor is retried only after it becomes active.\n\nTo retry the latest task regardless of the executor activation status:\n\n```ts\nretryInvalidated({ isEager: true });\n```\n\n## `retryRejected`\n\nRetries the last task after the execution has failed.\n\n```ts\nimport retryRejected from 'react-executor/plugin/retryRejected';\n\nconst executor = useExecutor('test', heavyTask, [retryRejected()]);\n\nexecutor.activate();\n```\n\nIf the task succeeds, is aborted, or if an executor is deactivated then the plugin stops the retry process.\n\nWith the default configuration, the plugin would retry the task 3 times with an exponential delay between retries.\n\nSpecify the number of times the task should be re-executed if it fails:\n\n```ts\nretryRejected({ count: 3 })\n```\n\nSpecify the delay in milliseconds between retries:\n\n```ts\nretryRejected({ count: 3, delay: 5_000 });\n```\n\nProvide a function that returns the delay depending on the number of retries:\n\n```ts\nretryRejected({\n  count: 5,\n  delay: (index, executor) =\u003e 1000 * 1.8 ** index\n});\n```\n\nBy default, `retryRejected` doesn't retry inactive executors. The executor is retried only after it becomes active.\n\nTo retry the latest task regardless of the executor activation status:\n\n```ts\nretryRejected({ isEager: true });\n```\n\n## `retryWhen`\n\n[Retries the latest task](#abort-a-task) if the\n[observable](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Observable.html) emits `true`.\n\nFor example, if the window was offline for more than 5 seconds, the executor would retry the `heavyTask` after\nthe window is back online:\n\n```ts\nimport retryWhen from 'react-executor/plugin/retryWhen';\nimport navigatorOnline from 'react-executor/observable/navigatorOnline';\n\nconst executor = useExecutor('test', heavyTask, [\n  retryWhen(navigatorOnline, { delay: 5_000 })\n]);\n```\n\nCombining multiple plugins, you can set up a complex executor behaviour. For example, let's create an executor that\nfollows these requirements:\n\n1. Executes the task every 5 seconds.\n2. Aborts the pending task if the window loses focus for more than 10 seconds.\n3. Aborts instantly if the window goes offline.\n4. Resumes the periodic task execution if window gains focus or goes back online.\n\n```ts\nimport { useExecutor } from 'react-executor';\nimport abortWhen from 'react-executor/plugin/abortWhen';\nimport retryWhen from 'react-executor/plugin/retryWhen';\nimport retryFulfilled from 'react-executor/plugin/retryFulfilled';\nimport windowFocused from 'react-executor/observable/windowFocused';\nimport windowBlurred from 'react-executor/observable/windowBlurred';\nimport navigatorOnline from 'react-executor/observable/navigatorOnline';\nimport navigatorOffline from 'react-executor/observable/navigatorOffline';\n\nuseExecutor('test', heavyTask, [\n\n  // Execute the task every 5 seconds\n  retryFulfilled({ delay: 5_000 }),\n  \n  // Abort the task and prevent future executions\n  // if the window looses focus for at least 10 seconds\n  abortWhen(windowBlurred, { delay: 10_000 }),\n\n  // Retry the latest task when the window gains focus\n  retryWhen(windowFocused),\n  \n  // Instantly abort the pending task if the device is disconnected from the network\n  abortWhen(navigatorOffline),\n\n  // Retry the latest task if the window goes online\n  retryWhen(navigatorOnline)\n]);\n```\n\n## `synchronizeStorage`\n\nPersists the executor value in the synchronous storage.\n\n```ts\nimport synchronizeStorage from 'react-executor/plugin/synchronizeStorage';\n\nconst executor = useExecutor('test', 42, [synchronizeStorage(localStorage)]);\n```\n\nWith this plugin, you can synchronize the executor state\n[across multiple browser tabs](https://codesandbox.io/p/sandbox/react-executor-example-ltflgy)\nin just one line.\n\n\u003e [!IMPORTANT]\\\n\u003e If executor is [detached](#detach-an-executor), then the corresponding item is removed from the storage.\n\nBy default, an executor state is serialized using\n[`JSON`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON). If your executor\nstores a value that may contain circular references, or non-serializable data like `BigInt`, use a custom serializer.\n\nHere's how you can enable serialization of objects with circular references:\n\n```ts\nimport JSONMarshal from 'json-marshal';\n\nconst executor = useExecutor('test', 42, [\n  synchronizeStorage(localStorage, {\n    serializer: JSONMarshal,\n  })\n]);\n```\n\n\u003e [!TIP]\\\n\u003e With additional configuration, [json-marshal](https://github.com/smikhalevski/json-marshal#readme) can stringify and\n\u003e parse any data structure.\n\nBy default, `synchronizeStorage` plugin uses a [serialized executor key](#executor-keys) as a storage key. You can\nprovide a custom key\nvia [`storageKey`](https://smikhalevski.github.io/react-executor/interfaces/plugin_synchronizeStorage.SynchronizeStorageOptions.html#storageKey)\noption:\n\n```ts\nuseExecutor('test', 42, [\n  synchronizeStorage(localStorage, { storageKey: 'helloBobr' })\n]);\n```\n\nIn the environment where storage is unavailable (for example, [during SSR](#server-side-rendering)), you can\nconditionally disable the plugin:\n\n```ts\nuseExecutor('test', 42, [\n  typeof localStorage !== 'undefined' ? synchronizeStorage(localStorage) : null\n]);\n```\n\n# React integration\n\nIn the basic scenario, to use executors in your React app, you don't need any additional configuration, just use\nthe [`useExecutor`](https://smikhalevski.github.io/react-executor/functions/react_executor.useExecutor.html) hook right\naway:\n\n```tsx\nimport { useExecutor } from 'react-executor';\n\nconst User = (props: { userId: string }) =\u003e {\n\n  const executor = useExecutor(['user', props.userId], async signal =\u003e {\n    // Fetch the user from the server\n  });\n  \n  if (executor.isPending) {\n    return 'Loading';\n  }\n  \n  // Render the user from the executor.value\n};\n```\n\nEvery time the executor's state is changed, the component is re-rendered. The executor returned from the hook is\n[activated](#activate-an-executor) after mount and deactivated on unmount.\n\nThe hook has the exact same signature as\nthe [`ExecutorManager.getOrCreate`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html#getOrCreate)\nmethod, described in the [Introduction](#introduction) section.\n\n\u003e [!TIP]\\\n\u003e Check out the live example\n\u003e of [the TODO app](https://codesandbox.io/p/sandbox/react-executor-example-ltflgy) that employs React Executor.\n\nYou can use executors both inside and outside the rendering process. To do this, provide a custom\n[`ExecutorManager`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html) through\nthe context:\n\n```tsx\nimport { ExecutorManager, ExecutorManagerProvider } from 'react-executor';\n\nconst manager = new ExecutorManager();\n\nconst App = () =\u003e (\n  \u003cExecutorManagerProvider value={manager}\u003e\n    \u003cUser userId={'28'}/\u003e\n  \u003c/ExecutorManagerProvider\u003e\n)\n```\n\nNow you can use `manager` to access all the same executors that are available through the `useExecutor` hook:\n\n```ts\nconst executor = manager.get(['user', '28']);\n```\n\nIf you want to have access to an executor in a component, but don't want to re-render the component when the executor's\nstate is changed,\nuse [`useExecutorManager`](https://smikhalevski.github.io/react-executor/functions/react_executor.useExecutorManager.html)\nhook:\n\n```ts\nconst accountExecutor = useExecutorManager().getOrCreate('account');\n```\n\nYou can execute a task in response to a user action, for example when user clicks a button:\n\n```tsx\nconst executor = useExecutor('test');\n\nconst handleClick = () =\u003e {\n  executor.execute(async signal =\u003e {\n    // Handle the task\n  });\n};\n```\n\nIf you want executor to run on the client only, then execute a task from the effect:\n\n```ts\nconst executor = useExecutor('test');\n\nuseEffect(() =\u003e {\n  executor.execute(async signal =\u003e {\n    // Handle the task\n  });\n}, []);\n```\n\n## Suspense\n\nExecutors support fetch-as-you-render approach and can be integrated with React Suspense. To facilitate the rendering\nsuspension, use the\n[`useExecutorSuspense`](https://smikhalevski.github.io/react-executor/functions/react_executor.useExecutorSuspense.html)\nhook:\n\n```tsx\nimport { useExecutor, useExecutorSuspense } from 'react-executor';\n\nconst Account = () =\u003e {\n  const accountExecutor = useExecutor('account', signal =\u003e {\n    // Fetch an account from a server\n  });\n  \n  // Suspend rendering if accountExecutor is pending and isn't fulfilled\n  const account = useExecutorSuspense(accountExecutor).get();\n};\n```\n\nNow when the `Account` component is rendered, it would be suspended until the `accountExecutor` is settled:\n\n```tsx\nimport { Suspense } from 'react';\n\nconst App = () =\u003e (\n  \u003cSuspense fallback={'Loading'}\u003e\n    \u003cAccount/\u003e\n  \u003c/Suspense\u003e\n);\n```\n\nExecutors can run tasks in parallel and rendering is suspended until both of them are settled:\n\n```ts\nconst cheeseExecutor = useExecutor('cheese', buyCheeseTask);\nconst beadExecutor = useExecutor('bread', bakeBreadTask);\n\nconst cheese = useExecutorSuspense(cheeseExecutor).get();\nconst bread = useExecutorSuspense(breadExecutor).get();\n```\n\n## External executors\n\nYou can use executors created outside the rendering process in your components, rerender and suspend your components\nwhen such executors get updated:\n\n```ts\nconst manager = new ExecutorManager();\n\n// 1️⃣ Create an executor\nconst accountExecutor = manager.getOrCreate('account', signal =\u003e {\n  // Fetch an account from a server\n});\n\nfunction Account() {\n  // 2️⃣ Re-render a component when accountExecutor is updated\n  useExecutorSubscription(accountExecutor);\n\n  // 3️⃣ Suspend rendering if accountExecutor is pending and isn't fulfilled\n  const account = useExecutorSuspense(accountExecutor).get();\n}\n```\n\n# Server-side rendering\n\n\u003e [!TIP]\\\n\u003e Check out the live example\n\u003e of [streaming SSR](https://codesandbox.io/p/devbox/react-executor-ssr-streaming-example-mwrmrs) with React Executor.\n\nExecutors can be hydrated on the client after being settled on the server.\n\nTo enable hydration on the client, create the executor manager and provide it through a context:\n\n```tsx\nimport React from 'react';\nimport { hydrateRoot } from 'react-dom/client';\nimport { enableSSRHydration, ExecutorManager, ExecutorManagerProvider } from 'react-executor';\n\nconst manager = new ExecutorManager();\n\n// 🟡 Hydrates executors on the client with the server data\nenableSSRHydration(manager);\n\nhydrateRoot(\n  document,\n  \u003cExecutorManagerProvider value={manager}\u003e\n    \u003cApp/\u003e\n  \u003c/ExecutorManagerProvider\u003e\n);\n```\n\nHere, `App` is the component that renders your application. Inside the `App` you can use `useExecutor` and\n[`useExecutorSuspence`](#suspense) to load your data.\n\n[`enableSSRHydration`](https://smikhalevski.github.io/react-executor/functions/react_executor.enableSSRHydration.html)\nmust be called only once, and only one manager on the client-side can receive the dehydrated state from the server.\n\nOn the server, you can either render your app contents [as a string](#render-to-string) and send it to the client in one\ngo, or [stream the contents](#streaming-ssr).\n\n## Render to string\n\nTo render your app as an HTML string\nuse [`SSRExecutorManager`](https://smikhalevski.github.io/react-executor/classes/ssr.SSRExecutorManager.html):\n\n```tsx\nimport { createServer } from 'http';\nimport { renderToString } from 'react-dom/server';\nimport { ExecutorManagerProvider } from 'react-executor';\nimport { SSRExecutorManager } from 'react-executor/ssr';\n\nconst server = createServer(async (request, response) =\u003e {\n\n  // 1️⃣ Create a new manager for each request\n  const manager = new SSRExecutorManager();\n\n  let html;\n  do {\n    html = renderToString(\n      \u003cExecutorManagerProvider value={manager}\u003e\n        \u003cApp/\u003e\n      \u003c/ExecutorManagerProvider\u003e\n    );\n\n    // 2️⃣ Render until there are no more changes\n  } while (await manager.hasChanges());\n\n  // 3️⃣ Attach dehydrated executor states\n  html += manager.nextHydrationChunk();\n\n  // 4️⃣ Send the rendered HTML to the client\n  response.end(html);\n});\n\nserver.listen(8080);\n```\n\nIn this example, the `App` is expected to render the `\u003cscript\u003e` tag that loads the client bundle. Otherwise, you can\ninject client chunk manually:\n\n```ts\nhtml += '\u003cscript src=\"/client.js\" async\u003e\u003c/script\u003e';\n```\n\nA new executor manager must be created for each request, so the results that are stored in executors are served in\nresponse to a particular request.\n\n[`hasChanges`](https://smikhalevski.github.io/react-executor/classes/ssr.SSRExecutorManager.html#hasChanges) would\nresolve with `true` if state of some executors have changed during rendering.\n\nThe hydration chunk returned\nby [`nextHydrationChunk`](https://smikhalevski.github.io/react-executor/classes/ssr.SSRExecutorManager.html#nextHydrationChunk)\ncontains the `\u003cscript\u003e` tag that hydrates the manager for which\n[`enableSSRHydration`](https://smikhalevski.github.io/react-executor/functions/react_executor.enableSSRHydration.html)\nwas invoked.\n\n## Streaming SSR\n\nThanks to [Suspense](#suspense), React can stream parts of your app while it is being rendered. React Executor provides\nAPI to inject its hydration chunks into a streaming process. The API is different for NodeJS streams and\n[Readable Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).\n\nIn NodeJS environment\nuse [`PipeableSSRExecutorManager`](https://smikhalevski.github.io/react-executor/classes/ssr_node.PipeableSSRExecutorManager.html)\n\n```tsx\nimport { createServer } from 'http';\nimport { renderToPipeableStream } from 'react-dom/server';\nimport { ExecutorManagerProvider } from 'react-executor';\nimport { PipeableSSRExecutorManager } from 'react-executor/ssr/node';\n\nconst server = createServer((request, response) =\u003e {\n\n  // 1️⃣ Create a new manager for each request\n  const manager = new PipeableSSRExecutorManager(response);\n\n  const stream = renderToPipeableStream(\n    \u003cExecutorManagerProvider value={manager}\u003e\n      \u003cApp/\u003e\n    \u003c/ExecutorManagerProvider\u003e,\n    {\n      bootstrapScripts: ['/client.js'],\n\n      onShellReady() {\n        // 2️⃣ Pipe the rendering output to the manager's stream \n        stream.pipe(manager.stream);\n      },\n    }\n  );\n});\n\nserver.listen(8080);\n```\n\nState of executors is streamed to the client along with the chunks rendered by React.\n\nIn the `App` component, use the combination of [`\u003cSuspense\u003e`](https://react.dev/reference/react/Suspense),\n[`useExecutor`](https://smikhalevski.github.io/react-executor/functions/react_executor.useExecutor.html) and\n[`useExecutorSuspence`](https://smikhalevski.github.io/react-executor/functions/react_executor.useExecutorSuspense.html)\nto suspend rendering while executors process their tasks:\n\n```tsx\nexport const App = () =\u003e (\n  \u003chtml\u003e\n    \u003chead/\u003e\n    \u003cbody\u003e\n      \u003cSuspense fallback={'Loading'}\u003e\n        \u003cHello/\u003e\n      \u003c/Suspense\u003e\n    \u003c/body\u003e\n  \u003c/html\u003e\n);\n\nexport const Hello = () =\u003e {\n  const helloExecutor = useExecutor('hello', async () =\u003e {\n    // Asynchronously return the result\n    return 'Hello, Paul!';\n  });\n\n  // 🟡 Suspend rendering until helloExecutor is settled\n  useExecutorSuspense(helloExecutor);\n\n  return helloExecutor.get();\n};\n```\n\nIf the `App` is rendered in streaming mode, it would first show \"Loading\" and after the executor is settled, it would\nupdate to \"Hello, Paul!\". In the meantime `helloExecutor` on the client would be hydrated with the data from the server.\n\n### Readable web streams support\n\nTo enable streaming in a modern environment,\nuse [`ReadableSSRExecutorManager`](https://smikhalevski.github.io/react-executor/classes/ssr.ReadableSSRExecutorManager.html)\n\n```tsx\nimport { renderToReadableStream } from 'react-dom/server';\nimport { ExecutorManagerProvider } from 'react-executor';\nimport { ReadableSSRExecutorManager } from 'react-executor/ssr';\n\nasync function handler(request) {\n\n  // 1️⃣ Create a new manager for each request\n  const manager = new ReadableSSRExecutorManager();\n\n  const stream = await renderToReadableStream(\n    \u003cExecutorManagerProvider value={manager}\u003e\n      \u003cApp /\u003e\n    \u003c/ExecutorManagerProvider\u003e,\n    {\n      bootstrapScripts: ['/client.js'],\n    }\n  );\n\n  // 2️⃣ Pipe the response through the manager\n  return new Response(stream.pipeThrough(manager), {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\nState of executors is streamed to the client along with the chunks rendered by React.\n\n## State serialization\n\nBy default, an executor state is serialized using\n[`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) that\nhas quite a few limitations. If your executor stores a value that may contain circular references, or non-serializable\ndata like `BigInt`, use a custom state serialization.\n\nOn the client, pass\na [`stateParser`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.SSRHydrationOptions.html#stateParser)\noption to `enableSSRHydration`:\n\n```tsx\nimport React from 'react';\nimport { hydrateRoot } from 'react-dom/client';\nimport { enableSSRHydration, ExecutorManager, ExecutorManagerProvider } from 'react-executor';\nimport JSONMarshal from 'json-marshal';\n\nconst manager = new ExecutorManager();\n\n// 🟡 Pass a custom state parser\nenableSSRHydration(manager, { stateParser: JSONMarshal.parse });\n\nhydrateRoot(\n  document,\n  \u003cExecutorManagerProvider value={manager}\u003e\n    \u003cApp/\u003e\n  \u003c/ExecutorManagerProvider\u003e\n);\n```\n\nOn the server, pass\na [`stateStringifier`](https://smikhalevski.github.io/react-executor/interfaces/ssr.SSRExecutorManagerOptions.html#stateStringifier)\noption to [`SSRExecutorManager`](#render-to-string),\n[`PipeableSSRExecutorManager`](#streaming-ssr),\nor [`ReadableSSRExecutorManager`](#readable-web-streams-support), depending on your setup:\n\n```ts\nimport { SSRExecutorManager } from 'react-executor/ssr';\nimport JSONMarshal from 'json-marshal';\n\nconst manager = new SSRExecutorManager({ stateStringifier: JSONMarshal.stringify });\n```\n\n\u003e [!TIP]\\\n\u003e With additional configuration, [json-marshal](https://github.com/smikhalevski/json-marshal#readme) can stringify and\n\u003e parse any data structure.\n\n## Content-Security-Policy support\n\nBy default,\n[`nextHydrationChunk`](https://smikhalevski.github.io/react-executor/classes/ssr.SSRExecutorManager.html#nextHydrationChunk)\nrenders an inline `\u003cscript\u003e` tag without any attributes. To enable the support of\nthe [`script-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src)\ndirective of the `Content-Security-Policy` header, provide\nthe [`nonce`](https://smikhalevski.github.io/react-executor/interfaces/ssr.SSRExecutorManagerOptions.html#nonce) option\nto `SSRExecutorManager` or any of its subclasses:\n\n```ts\nconst manager = new PipeableSSRExecutorManager(response, { nonce: '2726c7f26c' });\n```\n\nSend the header with this nonce in the server response:\n\n```\nContent-Security-Policy: script-src 'nonce-2726c7f26c'\n```\n\n## Next.js integration\n\n\u003e [!TIP]\\\n\u003e Check out the live example\n\u003e of [the Next.js app](https://codesandbox.io/p/devbox/react-executor-next-example-whsj4v) that showcases\n\u003e streaming SSR with React Executor.\n\nTo enable client hydration in Next.js, use [`@react-executor/next`](https://github.com/smikhalevski/react-executor-next)\npackage.\n\nFirst, provide\nan [`ExecutorManager`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html): \n\n```tsx\n// providers.tsx\n'use client';\n\nimport { ReactNode } from 'react';\nimport { enableSSRHydration, ExecutorManager, ExecutorManagerProvider } from 'react-executor';\nimport { SSRExecutorManager } from 'react-executor/ssr';\nimport { ExecutorHydrator } from '@react-executor/next';\n\nconst manager = typeof window !== 'undefined' ? enableSSRHydration(new ExecutorManager()) : undefined;\n\nexport function Providers(props: { children: ReactNode }) {\n  return (\n    \u003cExecutorManagerProvider value={manager || new SSRExecutorManager()}\u003e\n      \u003cExecutorHydrator\u003e{props.children}\u003c/ExecutorHydrator\u003e\n    \u003c/ExecutorManagerProvider\u003e\n  );\n}\n```\n\n`ExecutorHydrator` propagates server-rendered executor state to the client. You can configure how dehydrated state is\n[serialized on the server and deserialized on the client](#state-serialization), by default `JSON` is used.\n\nEnable providers in the root layout:\n\n```tsx\n// layout.tsx\nimport { ReactNode } from 'react';\nimport { Providers } from './providers';\n\nexport default function (props: { children: ReactNode }) {\n  return (\n    \u003chtml lang=\"en\"\u003e\n      \u003cbody\u003e\n        \u003cProviders\u003e{props.children}\u003c/Providers\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e\n  );\n}\n```\n\n# Devtools\n\nTo inspect the current state of executors in your app, install the\n[React Executor Devtools](https://chromewebstore.google.com/detail/react-executor-devtools/achlflelpafnlpepfpfhildkahbfhgjc)\nbrowser extension and open its panel in the Chrome Developer Tools:\n\n\u003cbr/\u003e\n\u003cp align=\"center\"\u003e\n  \u003cimg\n    alt=\"React Executor Devtools Screenshot\"\n    src=\"https://raw.githubusercontent.com/smikhalevski/react-executor-devtools/master/assets/screenshot.png\"\n    width=\"640\"\n  /\u003e\n\u003c/p\u003e\n\u003cbr/\u003e\n\nDevtools extension doesn't require any additional configuration and provides introspection to all executors on the\npage, regardless if they were rendered through React or created outside of the rendering process.\n\nTo disable devtools, create a custom\n[`ExecutorManager`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html):\n\n```ts\nimport { ExecutorManager } from 'react-executor';\n\nconst opaqueExecutorManager = new ExecutorManager({\n  devtools: false\n});\n```\n\nExecutors created by the `opaqueExecutorManager` won't be visible in the React Executor Devtools extension. It is\nrecommended to use this setting in production.\n\nThe extension source can be found in the [react-executor-devtools](https://github.com/smikhalevski/react-executor-devtools)\nrepo.\n\n# Cookbook\n\n## Optimistic updates\n\nTo implement optimistic updates, [resolve the executor](#settle-an-executor) with the expected value and then\nexecute a server request.\n\nFor example, if you want to instantly show to a user that a flag was enabled:\n\n```ts\nconst executor = useExecutor('flag', false);\n\nconst handleEnableClick = () =\u003e {\n  // 1️⃣ Optimistically resolve an executor\n  executor.resolve(true);\n\n  // 2️⃣ Synchronize state with the server\n  executor.execute(async signal =\u003e {\n    const response = await fetch('/flag', { signal });\n    \n    const data = await response.json();\n    \n    return data.isEnabled;\n  });\n};\n```\n\n## Dependent tasks\n\nPause a task until another executor is settled: \n\n```ts\nconst accountExecutor = useExecutor('account', async signal =\u003e {\n  // Fetch account here\n});\n\nconst shoppingCartExecutor = useExecutor('shoppingCart', async signal =\u003e {\n  const account = await accountExecutor.getOrAwait();\n  \n  // Fetch shopping cart for an account\n});\n```\n\nIn this example, the component is subscribed to both account and a shopping cart executors, and re-rendered if their\nstate is changed. To avoid unnecessary re-renders, you can acquire an executor through the manager:\n\n```tsx\nconst shoppingCartExecutor = useExecutor('shoppingCart', async (signal, executor) =\u003e {\n  \n  // 1️⃣ Wait for the account executor to be created\n  const accountExecutor = await executor.manager.getOrAwait('account');\n  \n  // 2️⃣ Wait for the account executor to be settled\n  const account = await accountExecutor.getOrAwait();\n\n  // Fetch shopping cart for an account\n});\n```\n\n## Pagination\n\nCreate an executor that would store the current page contents:\n\n```ts\nconst fetchPage = async (pageIndex: number, signal: AbortSignal) =\u003e {\n  // Request the data from the server here\n};\n\nconst pageExecutor = useExecutor('page', signal =\u003e fetchPage(0, signal));\n\nconst handleGoToPageClick = (pageIndex: number) =\u003e {\n  pageExecutor.execute(signal =\u003e fetchPage(pageIndex, signal));\n};\n```\n\nThe executor preserves the latest value it was resolved with, so you can render page contents using `executor.value`,\nand render a spinner when `executor.isPending`. \n\n## Infinite scroll\n\nCreate a task that uses the current executor value to combine it with the data loaded from the server:\n\n```ts\nconst itemsExecutor = useExecutor\u003cItem[]\u003e('items', async (signal, executor) =\u003e {\n  const items = executor.value || [];\n\n  return items.concat(await fetchItems({ offset: items.length, signal }));\n});\n```\n\nNow if a user clicks on a button to load more items, `itemsExecutor` must retry the latest task:\n\n```ts\nconst handleLoadMoreClick = () =\u003e {\n  itemsExecutor.retry();\n};\n```\n\n## Invalidate all executors\n\n[`ExecutorManager`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html#_iterator_)\nis iterable and provides access to all executors that it has created. You can perform bach operations with all executors\nin for-loop:\n\n```ts\nconst manager = useExecutorManager();\n\nfor (const executor of manager) {\n  executor.invalidate();\n}\n```\n\nBy default, invalidating an executor has no additional effect. If you want to\n[retry the latest task](#retry-the-latest-task) that each executor has executed, use\n[`retry`](https://smikhalevski.github.io/react-executor/interfaces/react_executor.Executor.html#retry):\n\n```ts\nfor (const executor of manager) {\n  executor.retry();\n}\n```\n\nIt isn't optimal to retry all executors even if they aren't [actively used](#activate-an-executor). Use the\n[`retryInvalidated`](https://smikhalevski.github.io/react-executor/modules/plugin_retryInvalidated.html) to retry active\nexecutors when they are invalidated.\n\n## Prefetching\n\nIn some cases, you can initialize an executor before its data is required for the first time:\n\n```ts\nconst User = () =\u003e {\n  useExecutorManager().getOrCreate('shoppingCart', fetchShoppingCart);\n};\n```\n\nIn this example, the executor with the `'shoppingCart'` key is initialized once the component is rendered for the first\ntime. The `User` component _won't be re-rendered_ if the state of this executor is changed.\n\nTo do prefetching before the application is even rendered, create an executor manager beforehand:\n\n```tsx\nconst manager = new ExecutorManager();\n\n// Prefetch the shopping cart\nmanager.getOrCreate('shoppingCart', fetchShoppingCart);\n\nconst App = () =\u003e (\n  \u003cExecutorManagerProvider value={manager}\u003e\n    {/* Render you app here */}\n  \u003c/ExecutorManagerProvider\u003e\n);\n```\n\n## Storage state versioning\n\nYou can store an executor state in a `localStorage` using the [`synchronizeStorage`](#synchronizestorage) plugin:\n\n```ts\nimport { useExecutor } from 'react-executor';\nimport synchronizeStorage from 'react-executor/plugin/synchronizeStorage';\n\nconst playerExecutor = useExecutor('player', { health: '50%' }, [synchronizeStorage(localStorage)]);\n// ⮕ Executor\u003c{ health: string }\u003e\n```\n\nBut what if over time you'd like to change the structure of the value stored in the `playerExecutor`? For example,\nmake `health` property a number:\n\n```ts\nconst playerExecutor = useExecutor('player', { health: 0.5 }, [synchronizeStorage(localStorage)]);\n```\n\nAfter users have used the previous version of the app where `health` was a string, they would still receive a string\nvalue since the `playerExecutor` state is read from the `localStorage`:\n\n```ts\nplayerExecutor.value.health\n// ⮕ '50%'\n```\n\nThis may lead to an unexpected behavior of your app. To mitigate this issue, let's write a plugin that would annotate\nthe executor with a version:\n\n```ts\nimport { type ExecutorPlugin } from 'react-executor';\n\nexport function requireVersion(version: number): ExecutorPlugin {\n  return executor =\u003e {\n    if (executor.annotations.version === version) {\n      // ✅ Executor is annotated with a correct version\n      return;\n    }\n\n    // ❌ Clear the executor state and annotate it with a proper version\n    executor.clear();\n    executor.annotate({ version });\n  };\n}\n```\n\nAdd the plugin to the executor:\n\n```ts\nconst playerExecutor = useExecutor('player', { health: 0.5 }, [\n  synchronizeStorage(localStorage),\n  requireVersion(1)\n]);\n```\n\nAfter the `synchronizeStorage` plugin reads the data from the `localStorage`, the `requireVersion` plugin ensures that\nthe `version` annotation read from the `localStorage` matches the required version. On mismatch the executor is cleared\nand the initial value `{ health: 0.5 }` is written to the storage.\n\n```ts\nplayerExecutor.value.health\n// ⮕ 0.5\n```\n\nBump the version provided to `requireVersion` plugin every time the structure of the executor value is changed.\n\nWe can enhance the `requireVersion` plugin by making it migrate the data instead of just clearing it:\n\n```ts\nexport function requireVersion\u003cT\u003e(version: number, migrate: (executor: Executor\u003cT\u003e) =\u003e T): ExecutorPlugin\u003cT\u003e {\n  return executor =\u003e {\n    if (executor.annotations.version === version) {\n      return;\n    }\n\n    // 🟡 Migrate only if executor has a value\n    if (executor.isSettled) {\n      migrate(executor);\n    }\n    \n    executor.annotate({ version });\n  };\n}\n```\n\nNow `requireVersion` would apply the migration on the state version mismatch: \n\n```ts\nconst playerExecutor = useExecutor('player', { health: 0.5 }, [\n  synchronizeStorage(localStorage),\n  \n  requireVersion(1, executor =\u003e {\n    executor.resolve({ \n      health: parseInt(executor.get().health) / 100\n    });\n  })\n]);\n```\n\n## Global loading indicator\n\nTo detect a global pending state we can rely on events published by\nan [`ExecutorManager`](https://smikhalevski.github.io/react-executor/classes/react_executor.ExecutorManager.html):\n\n```ts\nfunction useGlobalPending(predicate = (executor: Executor) =\u003e true): boolean {\n  const manager = useExecutorManager();\n  const [isPending, setPending] = useState(false);\n\n  useEffect(() =\u003e {\n    const listener = (event: ExecutorEvent) =\u003e {\n      setPending(\n        Array.from(manager)\n          .filter(predicate)\n          .some(executor =\u003e executor.isPending)\n      );\n    };\n\n    // 1️⃣ Ensure isPending is up-to-date after mount\n    listener();\n\n    // 2️⃣ Sync isPending when any event is published\n    return manager.subscribe(listener);\n  }, [manager]);\n  \n  return isPending;\n}\n```\n\nNow a global pending indicator can be shown when _any_ executor is pending:\n\n```tsx\nconst isPending = useGlobalPending();\n\nisPending \u0026\u0026 \u003cLoadingIndicator /\u003e;\n```\n\nYou can use a predicate to filter only executors that are actually fetching data. To do this, fetching executors should\nbe marked as such, for example with an annotation:\n\n```ts\nconst accountExecutor = useExecutor(\n  'account',\n\n  async () =\u003e {\n    const response = await fetch('/account');\n    return response.json();\n  },\n\n  // 1️⃣ Annotate an executor once via a plugin\n  [executor =\u003e executor.annotate({ isFetching: true })]\n);\n\n// 2️⃣ Get global pending status for executors that are fetching data\nconst isPending = useGlobalPending(executor =\u003e executor.annotations.isFetching);\n```\n\n\u003chr/\u003e\n\n\u003cp align=\"center\"\u003e\nIllustration by \u003ca href=\"https://www.slackart.com/\"\u003eMichael Slack\u003c/a\u003e\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmikhalevski%2Freact-executor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmikhalevski%2Freact-executor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmikhalevski%2Freact-executor/lists"}