diff --git a/.changeset/react-live-query-view-transitions.md b/.changeset/react-live-query-view-transitions.md new file mode 100644 index 0000000000..01cee30ddd --- /dev/null +++ b/.changeset/react-live-query-view-transitions.md @@ -0,0 +1,5 @@ +--- +'@tanstack/react-db': patch +--- + +Document how to animate live query results with React View Transitions in the React adapter guide and skill. diff --git a/docs/framework/react/overview.md b/docs/framework/react/overview.md index f57c8f1564..1525f08751 100644 --- a/docs/framework/react/overview.md +++ b/docs/framework/react/overview.md @@ -237,3 +237,76 @@ When a changed query plan creates a new live-query Collection, `useLiveSuspenseQuery` shows your Suspense fallback until its initial result is ready. Finite outer-window moves retain the Collection and follow its window publication policy. + +### Animating live query results with View Transitions + +Use React `` to animate rows entering, leaving, or moving in a +live query list. Pass the query's `data` to `useDeferredValue` and wrap each +rendered row in a `` with a stable, unique key. Use the result +row's `$key`, which also identifies individual rows in a join. If result keys +mix strings and numbers, include the type in the React key. For example, +`` `${typeof row.$key}:${row.$key}` `` keeps `1` and `'1'` distinct. +[Live query virtual properties](../../guides/live-queries.md#virtual-properties) + +`` is stable in React 19.3 and uses the browser's View Transition API. +[React View Transitions documentation](https://react.dev/reference/react/ViewTransition) + +`useLiveQuery` and `useLiveSuspenseQuery` subscribe through `useSyncExternalStore`. +React handles store updates synchronously, even when you call a collection +mutation inside `startTransition`. `useDeferredValue` lets React keep showing +the previous results and render the latest results in the background. React +can animate the change when that render commits. The same pattern works for +changes from local writes or incoming sync data. +[React external store caveats](https://react.dev/reference/react/useSyncExternalStore#caveats) · [React useDeferredValue documentation](https://react.dev/reference/react/useDeferredValue) + +```tsx +import { + collectionOptions, + localOnlyCollectionOptions, + useLiveQuery, +} from '@tanstack/react-db' +import { useDeferredValue, ViewTransition } from 'react' + +const todoCollection = collectionOptions( + localOnlyCollectionOptions({ + id: 'todos', + getKey: (todo) => todo.id, + initialData: [ + { id: 1, text: 'Write a note' }, + { id: 2, text: 'Water the plants' }, + ], + }), +) + +export function TodoList() { + const { data, isLoading } = useLiveQuery({ + query: (q) => + q.from({ todo: todoCollection }).orderBy(({ todo }) => todo.text), + }) + const visibleTodos = useDeferredValue(data) + + if (isLoading) return

Loading…

+ + return ( +
    + {visibleTodos.map((todo) => ( + +
  • {todo.text}
  • +
    + ))} +
+ ) +} +``` + +Respect the user's `prefers-reduced-motion` setting. To disable the row +animations, pass `default={prefersReducedMotion ? 'none' : 'auto'}` to each +``, where `prefersReducedMotion` is your app's current value +for that media query. +[React View Transition props](https://react.dev/reference/react/ViewTransition#view-transition-class) + +A deferred list can briefly show a row after deletion. If a row has a Delete +button, use the target collection's key for the record you want to delete. +Check `collection.has(key)` before calling `collection.delete(key)`. For a +joined result, `$key` identifies the result row for rendering. Read the record +key from the source you intend to delete from. diff --git a/docs/guides/mutations.md b/docs/guides/mutations.md index ba6f5d6e5c..a697727bb1 100644 --- a/docs/guides/mutations.md +++ b/docs/guides/mutations.md @@ -445,6 +445,8 @@ todoCollection.delete(todo.id, { optimistic: false }) **Returns**: A `Transaction` object that you can use to track the mutation's lifecycle. +For an exit animation in React, see [Animating live query results with View Transitions](../framework/react/overview.md#animating-live-query-results-with-view-transitions). + ## Operation Handlers Operation handlers are functions you provide when creating a collection that handle persisting mutations to your backend. Each collection can define three optional handlers: `onInsert`, `onUpdate`, and `onDelete`. diff --git a/packages/react-db/skills/react-db/SKILL.md b/packages/react-db/skills/react-db/SKILL.md index 3d0113ff77..33639a1f94 100644 --- a/packages/react-db/skills/react-db/SKILL.md +++ b/packages/react-db/skills/react-db/SKILL.md @@ -331,6 +331,22 @@ const { data } = useLiveQuery({ See meta-framework/SKILL.md for full preloading patterns. +### Animating live query results with View Transitions + +To animate rows entering, leaving, or moving, render `useDeferredValue(data)` +from `useLiveQuery` or `useLiveSuspenseQuery`. Wrap each rendered row in a +`` (stable in React 19.3) with a stable, unique result key such +as `row.$key`. Joined results use composite keys by default. When result keys +mix numbers and strings, use `` `${typeof row.$key}:${row.$key}` `` as the React +key. Wrapping a collection mutation in `startTransition` leaves the +`useSyncExternalStore` update synchronous. +Deferring the rendered results lets React animate changes from local writes +and incoming sync data. Use `prefers-reduced-motion` to reduce or disable +the animations. A deleted row can stay visible briefly. If it has a Delete +button, check `collection.has(key)` before deleting. Use the target +collection's record key for both calls. +See the [React adapter example](https://tanstack.com/db/latest/docs/framework/react/overview#animating-live-query-results-with-view-transitions). + ## Common Mistakes ### CRITICAL Using === instead of eq() in .where()