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
101 lines
3.4 KiB
Plaintext
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.
|