{"id":23770969,"url":"https://github.com/bubblydoo/react-done-tracker","last_synced_at":"2025-09-05T14:33:48.400Z","repository":{"id":64223234,"uuid":"574215282","full_name":"bubblydoo/react-done-tracker","owner":"bubblydoo","description":"Keep track of when an async tree is done rendering","archived":false,"fork":false,"pushed_at":"2025-05-26T17:04:53.000Z","size":3673,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-01T21:11:01.220Z","etag":null,"topics":["react","react-hooks","react-suspense"],"latest_commit_sha":null,"homepage":"https://react-done-tracker.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bubblydoo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2022-12-04T19:22:02.000Z","updated_at":"2025-05-26T17:04:56.000Z","dependencies_parsed_at":"2023-02-10T07:45:17.670Z","dependency_job_id":"18d11c1c-6047-4ce6-91a1-860bd57f920c","html_url":"https://github.com/bubblydoo/react-done-tracker","commit_stats":{"total_commits":78,"total_committers":1,"mean_commits":78.0,"dds":0.0,"last_synced_commit":"0e08854b219e2c40c1a79a40afc0c2febbbe2939"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bubblydoo/react-done-tracker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Freact-done-tracker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Freact-done-tracker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Freact-done-tracker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Freact-done-tracker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bubblydoo","download_url":"https://codeload.github.com/bubblydoo/react-done-tracker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Freact-done-tracker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273641953,"owners_count":25142268,"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","status":"online","status_checked_at":"2025-09-04T02:00:08.968Z","response_time":61,"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":["react","react-hooks","react-suspense"],"created_at":"2025-01-01T03:18:48.312Z","updated_at":"2025-09-05T14:33:43.369Z","avatar_url":"https://github.com/bubblydoo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ca href=\"https://www.npmjs.com/package/react-done-tracker\" alt=\"NPM\"\u003e\n  \u003cimg src=\"https://img.shields.io/npm/v/react-done-tracker\" alt=\"NPM\"/\u003e\n\u003c/a\u003e\n\u003ca href=\"https://react-done-tracker.vercel.app\" alt=\"Storybook\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Storybook-e1618c.svg?colorA=26077C\u0026logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAyCAYAAAD1CDOyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAX0SURBVGhD7ZlrbBRVFMfPnffuzrb0gVgQU6QYLBUtBY1EwBcaSST4ICI1CggmKn4SAhjkg6CJVZQvxC/yMiCBRIyGGBIDPiJoRAUfqBheItBAC5ay233Mzoz3zh6wOJ2Zu7O7oTH+msn93zvb7f3fx7lnpvBfgGB5Rdm3ZfvYxmmTh9q2XStJUr3Vfg66lq4DVRAha5mNEhE6qtYvfBo/7qJkJr5+cKE+KFIRr9Vi8Fempykuq2BY5iCBkDobYAD9Q00y7RRta5YEURAAZAugmnVAHnc9iTzzgJj/JgDr1FlILF2PNYcDlesWNKF2EWjiwpyVY7qz6QlRSRFoBxpoRyot266it5ok1hWAWtpJBb9IyheFodwzBrSZd2KtcBNOL/ygU/yRLqur6Ii+qYrSs7RspZ2fQq9r6W12RakB1vlQBhikIooqHIEm6JSXHaJrTmkePOGUhRJswrY6UJYNEtMgs/ULSL62BZIr3sNWfgJN0D1wHGXZSG/+DDI79jraPNIOyeWbHM0Lx0zQbVtm7K4Eqjx2xkDFR6CJjJlDVQACAWFADKRR9aDc3Qzq/eNoBGoGafQwIPEIfqh0BIbYxJyVC03bbsNqIMrEG0GZcgsIVw3AFjfWmS7IfLgHjK9+xZZAiguxFudqIpUxiC1rBW3Wvb4GGOx+ZN4UiL04A1uKI9gER5AlEZV26DEQ66/GFj7EhiGgL38Sa+EJNGFa1kmUnqgP3w7CwEqsFYYwpBbklhFYC0egiaistKPsE6IpoEwajbVwaE9MRhWOQBNZ00TVN9LNw+m68P4atomzO/eBeew0trhhEauY1CPQREc64bspxGHe+8BJ5JashfSmXZB8eSPk9h/GO26kkUNRFU6giaGxqu9Q9gmp0lG5yf18jGWQWKMn8we7UbkRBvpHND8CTdDcCZUHpvf9f0cru7MblRuWP4Ul8LA7N6tNF4lwAasu1GnjQZ16G9bcJF/dDOahU/kKIfk9ZOTAphfk6H4zTLBpafdkXOlHL4o77I50d7LCc3cbe35B1TexJTNAuW9svkKXVm7fIWeZsbTbPNwO5vEzzt7xMRBIoImx29oSdLo8z20WfXw7QEdfe3QS6K/MBnFwDTaWlkATjKDMI7nyfVTeCHXVEFsxC6LPT3NO+FJSEhPWyU5Ib9yJNX+k5uEQXz3fyWpLBZcJkRDcmd5kd+2H1NvbsRaMNvMuiC2YjrXiKMlMXMTYexASi9eA1XEeW/wRG6+lme/jzr4pBi4TgWdFL9hGTyx6B1IbPnFCaRBi/SDQHpmAtXBwmTBMswslN8bnP0L3/NVg7D6ALd4o9MmPJZJh4TKhSfJPKN2wteZ10ZlIrdkBPW1baZ01eKPccROqwuFajOdnv7GRFq352uXobXM9RzGxaA3YqYyjhZoK0F+f5+i+sE7SZPGly9769aa4E5thWN7pONFpGu1xgfLPS0HrbDd9pvY+3UlNHFXh8C0nUf4epRu/BHBwNao8vs8UQvgIxWXC9lnQLBp5oUxuQZVHvK4OlRs7lUVVOFwmcpb3aOd++xOVG5axRp+b6kQfVsq3jsQ7bi5luiHgMpHMZTxPr+zH36DqG6llBGjTJzqlH+zEDwuXiWo15hli7WQazN/Dvc2+SO7AH3RGw7/y5TKRtfxPXpbFsugTBpv+npNz+R8jvnCZOJ9NofKAHmrshYDxZfDpfAkaK7Kf/gCJZRvoU10aG8PBFddSc9/Ss6bp+YjaGxKPgjy+EeQxDSDQhyCiynSo6FjR4MAikHWiE4z9h5wnQrYUOSnuf3aM9FOr9IyV4zJRJoo/sXvMfOrQX+EycbT7LN+UXSG4TLRsa2NvAvrtdHCZ6O9wmygijJedQmbC9xX/laSg/XqsdblerUTgeLKroUqNyLqkgmlbctYyR6mCxN6KQNrM1QuEXCMJojNChmW1iDTNZj+WbdfRj8SYprDXAwrnDBd/TpSLbx9arNdoUail19EL526oVqN6XNbYYJBkLtui0IEQCR0KQk5XrH3hXfy1/+mnAPwNn4H/4oCgib8AAAAASUVORK5CYII=\" alt=\"Storybook\"/\u003e\n\u003c/a\u003e\n\n# React Done Tracker\n\nKeep track of when an async tree is done rendering.\n\n```bash\nnpm i react-done-tracker\n```\n\n## Example\n\n```tsx\nimport { TrackDone, useDoneTracker } from \"react-done-tracker\";\n\nfunction Image({ src }: { src: string }) {\n  const [loadedSrc, setLoadedSrc] = useState();\n\n  useDoneTracker({\n    done: loadedSrc === src,\n  });\n\n  return \u003cimg src={src} onLoad={(e) =\u003e setLoadedSrc(e.target.src)} /\u003e;\n}\n\nexport function App() {\n  return (\n    \u003cTrackDone onDone={() =\u003e console.log(\"✅\")}\u003e\n      \u003cImage src={\"https://picsum.photos/200\"} /\u003e\n    \u003c/TrackDone\u003e\n  );\n}\n```\n\nMore examples: see [Storybook](https://react-done-tracker.vercel.app)\n\n## What is React Done Tracker?\n\n### Done Tracker: a primitive for tracking the doneness of an async tree\n\nA done tracker is very simple. It has 4 states: `Pending`, `Done`, `Errored` and `Aborted`.\n\n```mermaid\nstateDiagram-v2\n    [*] --\u003e Pending\n    Pending --\u003e Done: done\n    Done --\u003e Pending: reset\n    Pending --\u003e Errored: error\n    Errored --\u003e Pending: reset\n    Pending --\u003e Aborted: abort\n```\n\nIf you use this library, every async action corresponds to one done tracker.\n\n### How do you change the state of a done tracker?\n\nThere are two types of done trackers:\n\n- Leafs (from `useDoneTracker`)\n- Nodes (when `useNodeDoneTracker`)\n\nThe rules are quite simple:\n\n- Leaf done trackers are done when they are signaled done.\n- Node done trackers are done when all of their children are done.\n\nLeaf done trackers can be signaled done with `doneTracker.signalDone()`.\n\n### What does that look like?\n\nTake for example:\n\n```tsx\n\u003cTrackDone\u003e\n  \u003cDelayedContainer delay={1000}\u003e\n    \u003cImage src={\"https://picsum.photos/200\"}\u003e\n    \u003cButton\u003eClick to make done\u003c/Button\u003e\n  \u003c/DelayedContainer\u003e\n  \u003cImage src={\"https://picsum.photos/200\"}\u003e\n\u003c/TrackDone\u003e\n```\n\nThis example would correspond to this tree of done trackers:\n\n```mermaid\ngraph TD\n    Root([Root]) --- DelayedContainer([DelayedContainer])\n    Root --- Image1\n    DelayedContainer --- Image2\n    DelayedContainer --- Button\n```\n\nThe node done trackers in the diagram have rounded corners.\n\n### How do I use it?\n\nThis library exposes many utilities to work with done trackers, most of them as React Hooks. Take a look at [Storybook](https://react-done-tracker.vercel.app) for many examples.\n\n### How does this compare to Suspense?\n\nSuspense is used for lazy loading data, and does not render anything to the DOM. React Done Tracker is made to wait for things to render to the DOM.\n\nFor example, you cannot use Suspense to wait for a slow canvas to render, or for a video to be loaded into a \u0026lt;video\u0026gt; element.\n\nLike Suspense, you can also re-suspend from inside the tree.\n\nYou can easily use Done Trackers and Suspense together, see [this example](https://react-done-tracker.vercel.app?path=/docs/contextual-api-suspense--docs).\n\n### How can I debug this?\n\nRun `window.__debug_react_done_tracker = true` before importing the library, and you will see logs of done tracker events, as well as the state of a done tracker tree when its doneness is being checked.\n\nYou can print the state of a done tracker tree to the console with `doneTracker.log()`.\n\nNext to that, the `useDoneTrackerRaw` hook uses `useDebugValue` which displays the done tracker state in React DevTools.\n\n### What if I have a node that doesn't render all its children immediately?\n\nIn this case you can add `skip: true` to the `useNodeDoneTracker` call until the children have been added.\n\ne.g.\n\n```tsx\nconst Tree = () =\u003e {\n  const [delaying, setDelaying] = useState(true);\n  useDoneTracker({\n    name: \"Async operation\",\n    done: !delaying,\n  });\n  const subtreeDoneTracker = useNodeDoneTracker({\n    name: \"Subtree\",\n    skip: delaying,\n  });\n\n  useEffect(() =\u003e {\n    if (!delaying) return;\n    const timeoutId = setTimeout(() =\u003e setDelaying(false), 2000);\n    return () =\u003e clearTimeout(timeoutId);\n  }, [delaying]);\n\n  if (delaying) return \u003c\u003eDelaying...\u003c/\u003e;\n  return (\n    \u003cDoneTrackerProvider doneTracker={subtreeDoneTracker}\u003e\n      \u003cDelayedComponent delay={1000} /\u003e\n    \u003c/DoneTrackerProvider\u003e\n  );\n}\n```\n\n## More examples\n\nIt's best to take a look at [Storybook](https://react-done-tracker.vercel.app) first to get a feeling of how this library can be used.\n\n### Creating a Root done tracker\n\nContextual API:\n\n```tsx\nimport { TrackDone } from \"react-done-tracker\";\n\nfunction App() {\n  return \u003cTrackDone onDone={...} onError={...}\u003e\n    \u003cImage src={\"https://picsum.photos/200\"}\u003e\n  \u003c/TrackDone\u003e\n}\n```\n\nImperative API:\n\n```tsx\nimport { ImperativeTrackDone } from \"react-done-tracker\";\n\nfunction App() {\n  return \u003cImperativeTrackDone onDone={...} onError={...}\u003e{(doneTracker) =\u003e (\n    \u003cImperativeImage src={\"https://picsum.photos/200\"} doneTracker={doneTracker}\u003e\n  )}\u003c/ImperativeTrackDone\u003e\n}\n```\n\n### Use a done tracker directly\n\nWhile you probably don't need to use the done trackers directly, they are quite simple and easy to use:\n\n```tsx\nconst child1 = new LeafDoneTracker();\nconst child2 = new LeafDoneTracker();\n\nconst parent = new NodeDoneTracker();\n\nparent.add(child1);\nparent.add(child2);\n\nchild1.signalDone();\n\nassert(!parent.done);\n\nchild2.signalDone();\n\nassert(parent.done);\n```\n\nWhen using `useDoneTracker`, you obtain a `LeafDoneTracker`.\n\nAborting a done tracker (e.g. `child.abort()`) removes it from the parent done tracker.\n\n```tsx\nconst child = new LeafDoneTracker();\nconst parent = new NodeDoneTracker();\n\nparent.add(child);\nchild.abort(); // used when a component is torn down\n\nassert(parent.done);\n```\n\nErrors are also supported:\n\n```tsx\nconst parent = new NodeDoneTracker();\nconst subparent = new NodeDoneTracker();\nconst child = new LeafDoneTracker();\n\nparent.add(subparent);\nsubparent.add(child);\n\nchild.signalError(\"some error\");\n\nassert(parent.errored);\n```\n\n### Using a node done tracker\n\nIn this example, we tap into the done tracker and set the background color based\non the state of the done tracker.\n\nContextual API:\n\n```tsx\nimport { TrackDone, DoneTrackerProvider, useNodeDoneTracker } from \"react-done-tracker\";\n\nfunction Tap({ children }) {\n  const doneTracker = useNodeDoneTracker({ name: \"Tap\" });\n\n  return (\n    \u003cdiv style={{ background: doneTracker.done ? \"green\" : \"black\" }}\u003e\n      \u003cDoneTrackerProvider doneTracker={doneTracker}\u003e\n        {props.children}\n      \u003c/DoneTrackerProvider\u003e\n    \u003c/div\u003e\n  );\n}\n\nfunction App() {\n  return \u003cTrackDone onDone={...} onError={...}\u003e\n    \u003cTap\u003e\n      \u003cButton /\u003e\n    \u003c/Tap\u003e\n  \u003c/TrackDone\u003e\n}\n```\n\n### Make a done tracked hook\n\n```tsx\n// from @tanstack/react-query\nconst useQueryDoneTracked = doneTrackHook(useQuery, { isDone: (result) =\u003e !result.isLoading });\n\n// from react-async-hook\nconst useAsyncDoneTracked = doneTrackHook(\n  useAsync,\n  { isDone: (result, args) =\u003e !result.loading \u0026\u0026 isEqual(result.currentParams, args[1]) }\n);\n```\n\n### Visualize the state of a subtree\n\nContextual API:\n\n```tsx\nimport { TrackDone, visualizeDoneWrapper} from \"react-done-tracker\";\n\nconst VisualizedImage = visualizeDoneWrapper(Image);\n\nfunction App() {\n  return \u003cTrackDone\u003e\n    \u003cVisualizedImage src={...}/\u003e\n  \u003c/TrackDone\u003e\n}\n```\n\n### Use an inline imperative leaf done tracker\n\n```tsx\nimport { ForkLeafDoneTracker } from \"react-done-tracker\";\n\n\u003cForkLeafDoneTracker\u003e\n  {(doneTracker) =\u003e (\n    \u003c\u003e\n      \u003cbutton onClick={() =\u003e doneTracker.signalDone()}\u003e✅ Done\u003c/button\u003e\n      \u003cbutton onClick={() =\u003e doneTracker.signalError(\"error\")}\u003e❌ Error\u003c/button\u003e\n    \u003c/\u003e\n  )}\n\u003c/ForkLeafDoneTracker\u003e\n```\n\n## Change event\n\nIn certain situations, it's useful to know when the children of a certain component have changed, e.g. when you want to screenshot those components after a change. On first load, you can wait for a `done` event. But when the children change in a non-async way, there will not be a second `done` event.\n\nBecause of that, you can trigger the `change` event if you want a parent component to know that the children have changed.\n\n```tsx\n// child\nuseEffect(() =\u003e {\n  doneTracker.signalChange();\n}, [doneTracker, dep]);\n\n// parent\nuseDoneTrackerSubscription(doneTracker, {\n  change: () =\u003e console.log(\"children have changed\")\n});\n```\n\nThe `change` event is not part of the \"core\" of this library. It was added because it's commonly needed.\n\n## Caveats\n\n### Slow hooks\n\nSlow hooks are hooks that don't update their loading state immediately.\n\nFor example:\n\n```tsx\nconst useSlow = (input: any) =\u003e {\n  const [output, setOutput] = useState\u003cany\u003e(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() =\u003e {\n    setLoading(true);\n    const timeoutId = setTimeout(() =\u003e {\n      setLoading(false);\n      setOutput(input);\n    }, 1000);\n    return () =\u003e clearTimeout(timeoutId);\n  }, [input]);\n\n  // (this hook could be fixed by using `loading = loading || input !== output`)\n  return [output, loading] as const;\n};\n\nconst Component = (props: { value: any }) =\u003e {\n  const [slowState, loading] = useSlow(props.value);\n\n  useDoneTracker({\n    name: \"Loading\",\n    done: !loading\n  });\n\n  return \u003c\u003e\n    \u003cLoadSomething value={props.value} /\u003e\n    \u003cdiv\u003e{slowState}\u003c/div\u003e\n  \u003c/\u003e\n}\n```\n\nIn this case, `useSlow2` is a slow hook, because the loading variable is delayed.\n\nThis can lead to problems in this case:\n- a new value comes in through the props\n- LoadSomething signals done immediately\n- this causes the root done tracker to recalculate its state\n- the Loading done tracker is done, because the loading variable is delayed\n- the root done tracker is done\n- the useEffect runs\n- the Loading done tracker resets\n- the root done tracker is pending again\n\nWe can fix it by making sure the loading state in `useSlow` is accurate:\n\n```ts\nconst actualLoading = loading || input !== output;\n\nreturn [output, actualLoading] as const;\n```\n\nFrom experience I know that there are a few slow hooks in the wild, like `react-async-hook`.\n\nThis library also provides utility functions to fix these kinds of \"misbehaving\" hooks:\n\n```ts\nimport isDeepEqual from \"fast-deep-equal\";\n\n// compare input and output to know if the hook is done (preferred)\n// this is not always possible, because the result and args are not always easily comparable\nconst useAsyncFixed = doneTrackHook(\n  useAsync,\n  { isDone: (result, args) =\u003e isDeepEqual(result.currentParams, args[1]) }\n);\n\n// wait 2 extra useEffect cycles on each change (less preferred)\nconst useAsyncFixed = doneTrackSlowHookWithEffectsDelay(\n  useAsync,\n  {\n    waitEffects: 2,\n    argsEqual: (a, b) =\u003e isDeepEqual(a[1], b[1]),\n  }\n);\n\n// wait 100ms on each args change (not preferred, very dirty)\nconst useAsyncFixed = doneTrackSlowHookWithDelay(\n  useAsync,\n  {\n    delay: 100,\n    argsEqual: (a, b) =\u003e isDeepEqual(a[1], b[1]),\n  }\n);\n```\n\nYou will likely never see this problem unless you heavily use this library, but it is worth being aware of.\nA warning will be logged when the time between done and reset is very short, in order to debug the root cause.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbubblydoo%2Freact-done-tracker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbubblydoo%2Freact-done-tracker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbubblydoo%2Freact-done-tracker/lists"}