Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

101 lines
3.4 KiB
Plaintext

---
title: Lifecycle
description: How resources render, mount, update, and unmount.
---
## Render and commit
Like React, tap splits work into two phases:
1. **Render**: the resource function runs, hooks record their data, and a return value is produced. This phase has no side effects.
2. **Commit**: effects whose dependencies changed run. If an effect is re-running, its previous cleanup executes first. The resource is considered mounted after this phase.
```ts
import { resource } from "@assistant-ui/tap";
import { useState, useEffect } from "react";
const useApp = () => {
// --- render phase ---
const [count, setCount] = useState(0);
useEffect(() => {
// --- commit phase ---
console.log(`Mounted with count: ${count}`);
return () => console.log("Cleaned up");
}, [count]);
return { count };
};
const App = resource(useApp);
```
### Effect ordering
In React, effects run children-first, then parents (inside-out). This is because components can only render children by returning them, there's no way to run an effect after a child's effects.
In tap, **effects run in the exact order they are called during the render pass**. Since `useResource` is just another hook, you can place `useEffect` calls before or after it:
```ts
import { resource, useResource } from "@assistant-ui/tap";
import { useEffect } from "react";
const useParent = () => {
useEffect(() => {
console.log("1: before child");
});
const child = useResource(Child());
useEffect(() => {
console.log("3: after child");
});
return child;
};
const Parent = resource(useParent);
const useChild = () => {
useEffect(() => {
console.log("2: child");
});
};
const Child = resource(useChild);
// Mount order: 1, 2, 3
```
This is intentional, it lets you run setup logic both before and after children, which is useful when a parent needs to react to data provided by a child.
Cleanup on unmount runs in the same order as mount (FIFO), matching the order effects were originally registered.
## Mount and unmount
A resource is **mounted** after its first commit. At this point, effects have run and the resource is live.
A resource is **unmounted** when its owner removes it, all effect cleanups run and the resource is disposed. This happens automatically when:
- A parent resource stops rendering the child via `useResource`
- A React component using `useResource` unmounts
- `root.unmount()` is called on a `createTapRoot` root
## Concurrent mode
Tap has full interoperability with React's concurrent mode. Resources rendered via `useResource` work correctly with `startTransition`, `useDeferredValue`, `<Suspense>`, and other concurrent features, state updates are applied consistently without tearing.
<Callout type="warn">
The `@assistant-ui/store` layer currently relies on `useSyncExternalStore` to bridge tap resources into React. This means store subscriptions always trigger synchronous re-renders, opting out of concurrent scheduling for those reads. This may change in a future release.
</Callout>
## Offscreen / Activity
When a resource is hidden by `<Activity mode="hidden">`, state updates that arrive while it's hidden are tracked and replayed once it becomes visible again. No configuration needed.
## Strict mode
In development, resources render twice on mount to surface side effects in render, the same as React's `<StrictMode>`.
- **`useResource`** inherits the surrounding `<StrictMode>`.
- **`createTapRoot`** enables it automatically in development.