Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/bubblydoo/react-done-tracker
Keep track of when an async tree is done rendering
https://github.com/bubblydoo/react-done-tracker
react react-hooks react-suspense
Last synced: 8 days ago
JSON representation
Keep track of when an async tree is done rendering
- Host: GitHub
- URL: https://github.com/bubblydoo/react-done-tracker
- Owner: bubblydoo
- Created: 2022-12-04T19:22:02.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2023-10-31T13:29:45.000Z (about 1 year ago)
- Last Synced: 2024-12-24T21:33:53.681Z (15 days ago)
- Topics: react, react-hooks, react-suspense
- Language: TypeScript
- Homepage: https://react-done-tracker.vercel.app
- Size: 2.6 MB
- Stars: 4
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# React Done Tracker
Keep track of when an async tree is done rendering.
```bash
npm i react-done-tracker
```## Example
```tsx
import { TrackDone, useDoneTracker } from "react-done-tracker";function Image({ src }: { src: string }) {
const [loadedSrc, setLoadedSrc] = useState();useDoneTracker({
done: loadedSrc === src,
});return setLoadedSrc(e.target.src)} />;
}export function App() {
return (
console.log("✅")}>
);
}
```More examples: see [Storybook](https://react-done-tracker.vercel.app)
## What is React Done Tracker?
### Done Tracker: a primitive for tracking the doneness of an async tree
A done tracker is very simple. It has 4 states: `Pending`, `Done`, `Errored` and `Aborted`.
```mermaid
stateDiagram-v2
[*] --> Pending
Pending --> Done: done
Done --> Pending: reset
Pending --> Errored: error
Errored --> Pending: reset
Pending --> Aborted: abort
```If you use this library, every async action corresponds to one done tracker.
### How do you change the state of a done tracker?
There are two types of done trackers:
- Leafs (from `useDoneTracker`)
- Nodes (when `useNodeDoneTracker`)The rules are quite simple:
- Leaf done trackers are done when they are signaled done.
- Node done trackers are done when all of their children are done.Leaf done trackers can be signaled done with `doneTracker.signalDone()`.
### What does that look like?
Take for example:
```tsx
Click to make done
```
This example would correspond to this tree of done trackers:
```mermaid
graph TD
Root([Root]) --- DelayedContainer([DelayedContainer])
Root --- Image1
DelayedContainer --- Image2
DelayedContainer --- Button
```The node done trackers in the diagram have rounded corners.
### How do I use it?
This 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.
### How does this compare to Suspense?
Suspense 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.
For example, you cannot use Suspense to wait for a slow canvas to render, or for a video to be loaded into a <video> element.
Like Suspense, you can also re-suspend from inside the tree.
You can easily use Done Trackers and Suspense together, see [this example](https://react-done-tracker.vercel.app?path=/docs/contextual-api-suspense--docs).
### How can I debug this?
Run `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.
You can print the state of a done tracker tree to the console with `doneTracker.log()`.
Next to that, the `useDoneTrackerRaw` hook uses `useDebugValue` which displays the done tracker state in React DevTools.
### What if I have a node that doesn't render all its children immediately?
In this case you can add `skip: true` to the `useNodeDoneTracker` call until the children have been added.
e.g.
```tsx
const Tree = () => {
const [delaying, setDelaying] = useState(true);
useDoneTracker({
name: "Async operation",
done: !delaying,
});
const subtreeDoneTracker = useNodeDoneTracker({
name: "Subtree",
skip: delaying,
});useEffect(() => {
if (!delaying) return;
const timeoutId = setTimeout(() => setDelaying(false), 2000);
return () => clearTimeout(timeoutId);
}, [delaying]);if (delaying) return <>Delaying...>;
return (
);
}
```## More examples
It'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.
### Creating a Root done tracker
Contextual API:
```tsx
import { TrackDone } from "react-done-tracker";function App() {
return
}
```Imperative API:
```tsx
import { ImperativeTrackDone } from "react-done-tracker";function App() {
return {(doneTracker) => (
)}
}
```### Use a done tracker directly
While you probably don't need to use the done trackers directly, they are quite simple and easy to use:
```tsx
const child1 = new LeafDoneTracker();
const child2 = new LeafDoneTracker();const parent = new NodeDoneTracker();
parent.add(child1);
parent.add(child2);child1.signalDone();
assert(!parent.done);
child2.signalDone();
assert(parent.done);
```When using `useDoneTracker`, you obtain a `LeafDoneTracker`.
Aborting a done tracker (e.g. `child.abort()`) removes it from the parent done tracker.
```tsx
const child = new LeafDoneTracker();
const parent = new NodeDoneTracker();parent.add(child);
child.abort(); // used when a component is torn downassert(parent.done);
```Errors are also supported:
```tsx
const parent = new NodeDoneTracker();
const subparent = new NodeDoneTracker();
const child = new LeafDoneTracker();parent.add(subparent);
subparent.add(child);child.signalError("some error");
assert(parent.errored);
```### Using a node done tracker
In this example, we tap into the done tracker and set the background color based
on the state of the done tracker.Contextual API:
```tsx
import { TrackDone, DoneTrackerProvider, useNodeDoneTracker } from "react-done-tracker";function Tap({ children }) {
const doneTracker = useNodeDoneTracker({ name: "Tap" });return (
{props.children}
);
}function App() {
return
}
```### Make a done tracked hook
```tsx
// from @tanstack/react-query
const useQueryDoneTracked = doneTrackHook(useQuery, { isDone: (result) => !result.isLoading });// from react-async-hook
const useAsyncDoneTracked = doneTrackHook(
useAsync,
{ isDone: (result, args) => !result.loading && isEqual(result.currentParams, args[1]) }
);
```### Visualize the state of a subtree
Contextual API:
```tsx
import { TrackDone, visualizeDoneWrapper} from "react-done-tracker";const VisualizedImage = visualizeDoneWrapper(Image);
function App() {
return
}
```### Use an inline imperative leaf done tracker
```tsx
import { ForkLeafDoneTracker } from "react-done-tracker";{(doneTracker) => (
<>
doneTracker.signalDone()}>✅ Done
doneTracker.signalError("error")}>❌ Error
>
)}```
## Change event
In 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.
Because of that, you can trigger the `change` event if you want a parent component to know that the children have changed.
```tsx
// child
useEffect(() => {
doneTracker.signalChange();
}, [doneTracker, dep]);// parent
useDoneTrackerSubscription(doneTracker, {
change: () => console.log("children have changed")
});
```The `change` event is not part of the "core" of this library. It was added because it's commonly needed.
## Caveats
### Slow hooks
Slow hooks are hooks that don't update their loading state immediately.
For example:
```tsx
const useSlow = (input: any) => {
const [output, setOutput] = useState(null);
const [loading, setLoading] = useState(true);useEffect(() => {
setLoading(true);
const timeoutId = setTimeout(() => {
setLoading(false);
setOutput(input);
}, 1000);
return () => clearTimeout(timeoutId);
}, [input]);// (this hook could be fixed by using `loading = loading || input !== output`)
return [output, loading] as const;
};const Component = (props: { value: any }) => {
const [slowState, loading] = useSlow(props.value);useDoneTracker({
name: "Loading",
done: !loading
});return <>
{slowState}
>
}
```In this case, `useSlow2` is a slow hook, because the loading variable is delayed.
This can lead to problems in this case:
- a new value comes in through the props
- LoadSomething signals done immediately
- this causes the root done tracker to recalculate its state
- the Loading done tracker is done, because the loading variable is delayed
- the root done tracker is done
- the useEffect runs
- the Loading done tracker resets
- the root done tracker is pending againWe can fix it by making sure the loading state in `useSlow` is accurate:
```ts
const actualLoading = loading || input !== output;return [output, actualLoading] as const;
```From experience I know that there are a few slow hooks in the wild, like `react-async-hook`.
This library also provides utility functions to fix these kinds of "misbehaving" hooks:
```ts
import isDeepEqual from "fast-deep-equal";// compare input and output to know if the hook is done (preferred)
// this is not always possible, because the result and args are not always easily comparable
const useAsyncFixed = doneTrackHook(
useAsync,
{ isDone: (result, args) => isDeepEqual(result.currentParams, args[1]) }
);// wait 2 extra useEffect cycles on each change (less preferred)
const useAsyncFixed = doneTrackSlowHookWithEffectsDelay(
useAsync,
{
waitEffects: 2,
argsEqual: (a, b) => isDeepEqual(a[1], b[1]),
}
);// wait 100ms on each args change (not preferred, very dirty)
const useAsyncFixed = doneTrackSlowHookWithDelay(
useAsync,
{
delay: 100,
argsEqual: (a, b) => isDeepEqual(a[1], b[1]),
}
);
```You will likely never see this problem unless you heavily use this library, but it is worth being aware of.
A warning will be logged when the time between done and reset is very short, in order to debug the root cause.