diff --git a/docs/core/api/useSuspense.md b/docs/core/api/useSuspense.md
index bf6866d0a99c..ba31ecd81810 100644
--- a/docs/core/api/useSuspense.md
+++ b/docs/core/api/useSuspense.md
@@ -134,12 +134,12 @@ render();
Cache policy is [Stale-While-Revalidate](https://tools.ietf.org/html/rfc5861) by default but also [configurable](../concepts/expiry-policy.md).
-| Expiry Status | Fetch | Suspend | Error | Conditions |
-| ------------- | --------------- | ------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Expiry Status | Fetch | Suspend | Error | Conditions |
+| ------------- | --------------- | ------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Invalid | yes1 | yes | no | not in store, [deletion](/rest/api/resource#delete), [invalidation](./Controller.md#invalidate), [invalidIfStale](../concepts/expiry-policy.md#endpointinvalidifstale) |
-| Stale | yes1 | no | no | (first-render, arg change) & [expiry < now](../concepts/expiry-policy.md) |
-| Valid | no | no | maybe2 | fetch completion |
-| | no | no | no | `null` used as second argument |
+| Stale | yes1 | no | no | (first-render, arg change) & [expiry < now](../concepts/expiry-policy.md) |
+| Valid | no | no | maybe2 | fetch completion |
+| | no | no | no | `null` used as second argument |
:::note
@@ -348,11 +348,7 @@ export const getPosts = new RestEndpoint({
import { getPosts } from './api/Post';
export default function ArticleList({ page }: { page: string }) {
- const {
- posts,
- nextPage,
- lastPage,
- } = useSuspense(getPosts, { page });
+ const { posts, nextPage, lastPage } = useSuspense(getPosts, { page });
return (
{posts.map(post => (
@@ -388,4 +384,4 @@ less intrusive _loading bar_, like [YouTube](https://youtube.com) and [Robinhood
-If you need help adding this to your own custom router, check out the [official React guide](https://react.dev/reference/react/useTransition#building-a-suspense-enabled-router)
\ No newline at end of file
+If you need help adding this to your own custom router, check out the [official React guide](https://react.dev/reference/react/useTransition#building-a-suspense-enabled-router)
diff --git a/docs/core/api/useSuspense.vue.md b/docs/core/api/useSuspense.vue.md
new file mode 100644
index 000000000000..477b14866141
--- /dev/null
+++ b/docs/core/api/useSuspense.vue.md
@@ -0,0 +1,367 @@
+---
+title: useSuspense() - Simplified data fetching for Vue
+sidebar_label: useSuspense()
+description: High performance async data rendering without overfetching. useSuspense() is like await for Vue components.
+---
+
+
+
+
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import GenericsTabs from '@site/src/components/GenericsTabs';
+import ConditionalDependencies from '../shared/\_conditional_dependencies.vue.mdx';
+import PaginationDemo from '../shared/\_pagination.vue.mdx';
+import TypeScriptEditor from '@site/src/components/TypeScriptEditor';
+import { detailFixtures, listFixtures } from '@site/src/fixtures/profiles';
+
+# useSuspense()
+
+
+High performance async data rendering without overfetching.
+
+
+[await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) `useSuspense()` in Vue components. This means the remainder of the component only runs after the data has loaded, avoiding the complexity of handling loading and error conditions. Instead, fallback handling is
+[centralized](../getting-started/data-dependency.md#boundaries) with Vue's built-in [Suspense](https://vuejs.org/guide/built-ins/suspense.html).
+
+`useSuspense()` is reactive to data [mutations](../getting-started/mutations.md); rerendering only when necessary.
+
+## Usage
+
+
+
+
+
+
+```typescript title="ProfileResource" collapsed
+import { Entity, resource } from '@data-client/rest';
+
+export class Profile extends Entity {
+ id: number | undefined = undefined;
+ avatar = '';
+ fullName = '';
+ bio = '';
+
+ static key = 'Profile';
+}
+
+export const ProfileResource = resource({
+ path: '/profiles/:id',
+ schema: Profile,
+});
+```
+
+```html title="ProfileDetail.vue"
+
+
+
+
+
+```
+
+
+
+
+
+
+## Behavior
+
+Cache policy is [Stale-While-Revalidate](https://tools.ietf.org/html/rfc5861) by default but also [configurable](../concepts/expiry-policy.md).
+
+| Expiry Status | Fetch | Suspend | Error | Conditions |
+| ------------- | --------------- | ------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Invalid | yes1 | yes | no | not in store, [deletion](/rest/api/resource#delete), [invalidation](./Controller.md#invalidate), [invalidIfStale](../concepts/expiry-policy.md#endpointinvalidifstale) |
+| Stale | yes1 | no | no | (first-render, arg change) & [expiry < now](../concepts/expiry-policy.md) |
+| Valid | no | no | maybe2 | fetch completion |
+| | no | no | no | `null` used as second argument |
+
+:::note
+
+1. Identical fetches are automatically deduplicated
+2. [Hard errors](../concepts/error-policy.md#hard) to be [caught](../getting-started/data-dependency#async-fallbacks) by [Error Boundaries](./AsyncBoundary.md)
+
+:::
+
+
+
+## Types
+
+
+
+```typescript
+function useSuspense(
+ endpoint: ReadEndpoint,
+ ...args: Parameters | [null]
+): Denormalize;
+```
+
+```typescript
+function useSuspense<
+ E extends EndpointInterface<
+ FetchFunction,
+ Schema | undefined,
+ undefined
+ >,
+ Args extends readonly [...Parameters] | readonly [null],
+>(
+ endpoint: E,
+ ...args: Args
+): E['schema'] extends Exclude
+ ? Denormalize
+ : ReturnType;
+```
+
+
+
+## Examples
+
+### List
+
+
+
+```typescript title="ProfileResource" collapsed
+import { Entity, resource } from '@data-client/rest';
+
+export class Profile extends Entity {
+ id: number | undefined = undefined;
+ avatar = '';
+ fullName = '';
+ bio = '';
+
+ static key = 'Profile';
+}
+
+export const ProfileResource = resource({
+ path: '/profiles/:id',
+ schema: Profile,
+});
+```
+
+```html title="ProfileList.vue"
+
+
+
+
+
+
+
+
{{ profile.fullName }}
+
{{ profile.bio }}
+
+
+
+
+```
+
+
+
+### Pagination
+
+Reactive [pagination](/rest/guides/pagination) is achieved with [mutable schemas](/rest/api/Collection)
+
+
+
+### Sequential
+
+When fetch parameters depend on data from another resource.
+
+```html
+
+```
+
+### Conditional
+
+`null` will avoid binding and fetching data
+
+
+
+```ts title="Resources" collapsed
+import { Entity, resource } from '@data-client/rest';
+
+export class Post extends Entity {
+ id = 0;
+ userId = 0;
+ title = '';
+ body = '';
+
+ static key = 'Post';
+}
+export const PostResource = resource({
+ path: '/posts/:id',
+ schema: Post,
+});
+
+export class User extends Entity {
+ id = 0;
+ name = '';
+ username = '';
+ email = '';
+ phone = '';
+ website = '';
+
+ get profileImage() {
+ return `https://i.pravatar.cc/64?img=${this.id + 4}`;
+ }
+
+ static key = 'User';
+}
+export const UserResource = resource({
+ urlPrefix: 'https://jsonplaceholder.typicode.com',
+ path: '/users/:id',
+ schema: User,
+});
+```
+
+```html title="PostWithAuthor.vue" {10-16}
+
+
+
+
+
+
+
+```
+
+
+
+### Embedded data
+
+When entities are stored in [nested structures](/rest/guides/relational-data#nesting), that structure will remain.
+
+
+
+```typescript title="api/Post" {12-16}
+export class PaginatedPost extends Entity {
+ id = '';
+ title = '';
+ content = '';
+
+ static key = 'PaginatedPost';
+}
+
+export const getPosts = new RestEndpoint({
+ path: '/post',
+ searchParams: { page: '' },
+ schema: {
+ posts: new schema.Collection([PaginatedPost]),
+ nextPage: '',
+ lastPage: '',
+ },
+});
+```
+
+```html title="ArticleList.vue"
+
+
+
+
+
{{ post.title }}
+
+
+```
+
+
+
diff --git a/docs/core/getting-started/data-dependency.md b/docs/core/getting-started/data-dependency.md
index f3f0d76f47a9..96d7b9544b5d 100644
--- a/docs/core/getting-started/data-dependency.md
+++ b/docs/core/getting-started/data-dependency.md
@@ -203,8 +203,7 @@ bound components immediately upon [data change](./mutations.md). This is known a
## Loading and Error {#async-fallbacks}
-You might have noticed the return type shows the value is always there. [useSuspense()](../api/useSuspense.md) operates very much
-like [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await). This enables
+You might have noticed the return type shows the value is always there. [useSuspense()](../api/useSuspense.md) operates very much like [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await). This enables
us to make error/loading disjoint from data usage.
### Async Boundaries {#boundaries}
@@ -214,7 +213,7 @@ routes, or [modals](https://www.appcues.com/blog/modal-dialog-windows)**.
-React 18's [useTransition](https://react.dev/reference/react/useTransition) and [Server Side Rendering](../guides/ssr.md)
+React 18+'s [useTransition](https://react.dev/reference/react/useTransition) and [Server Side Rendering](../guides/ssr.md)
powered routers or navigation means never seeing a loading fallback again. In React 16 and 17 fallbacks can be centralized
to eliminate redundant loading indicators while keeping components reusable.
@@ -278,7 +277,7 @@ render();
Since [useDLE](../api/useDLE.md) does not [useSuspense](../api/useSuspense.md), you won't be able to easily centrally
-orchestrate loading and error code. Additionally, React 18 features like [useTransition](https://react.dev/reference/react/useTransition),
+orchestrate loading and error code. Additionally, React 18+ features like [useTransition](https://react.dev/reference/react/useTransition),
and [incrementally streaming SSR](../guides/ssr.md) won't work with components that use it.
## Conditional
diff --git a/docs/core/getting-started/data-dependency.vue.md b/docs/core/getting-started/data-dependency.vue.md
new file mode 100644
index 000000000000..dbac3e383a29
--- /dev/null
+++ b/docs/core/getting-started/data-dependency.vue.md
@@ -0,0 +1,254 @@
+---
+title: Rendering Asynchronous Data in Vue
+sidebar_label: Render Data
+---
+
+
+
+
+
+import ThemedImage from '@theme/ThemedImage';
+import useBaseUrl from '@docusaurus/useBaseUrl';
+import TypeScriptEditor from '@site/src/components/TypeScriptEditor';
+import ConditionalDependencies from '../shared/\_conditional_dependencies.vue.mdx';
+import { postFixtures } from '@site/src/fixtures/posts';
+import { detailFixtures, listFixtures } from '@site/src/fixtures/profiles';
+import UseLive from '../shared/\_useLive.vue.mdx';
+import AsyncBoundaryExamples from '../shared/\_AsyncBoundary.vue.mdx';
+
+# Rendering Asynchronous Data
+
+Make your components reusable by binding the data where you **use** it with the one-line [useSuspense()](../api/useSuspense.md),
+which guarantees data with [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await).
+
+
+
+```ts title="Resources" collapsed
+import { Entity, resource } from '@data-client/rest';
+
+export class User extends Entity {
+ id = 0;
+ name = '';
+ username = '';
+ email = '';
+ phone = '';
+ website = '';
+
+ get profileImage() {
+ return `https://i.pravatar.cc/64?img=${this.id + 4}`;
+ }
+
+ static key = 'User';
+}
+export const UserResource = resource({
+ urlPrefix: 'https://jsonplaceholder.typicode.com',
+ path: '/users/:id',
+ schema: User,
+});
+
+export class Post extends Entity {
+ id = 0;
+ author = User.fromJS();
+ title = '';
+ body = '';
+
+ static key = 'Post';
+
+ static schema = {
+ author: User,
+ };
+}
+export const PostResource = resource({
+ path: '/posts/:id',
+ schema: Post,
+ paginationField: 'page',
+});
+```
+
+```html title="PostDetail.vue" collapsed
+
+
+
+
+
+```
+
+
+
+
+
+
+
+Do not [prop drill](https://react.dev/learn/passing-data-deeply-with-context#the-problem-with-passing-props). Instead, [useSuspense()](../api/useSuspense.md) in the components that render the data from it. This is
+known as _data co-location_.
+
+Instead of writing complex update functions or invalidations cascades, Reactive Data Client automatically updates
+bound components immediately upon [data change](./mutations.md). This is known as _reactive programming_.
+
+## Loading and Error {#async-fallbacks}
+
+You might have noticed the return type shows the value is always there. [useSuspense()](../api/useSuspense.md) operates very much with [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await). This enables
+us to make error/loading disjoint from data usage.
+
+### Async Boundaries {#boundaries}
+
+Instead we place [<AsyncBoundary /\>](../api/AsyncBoundary.md) to handling loading and error conditions at or above navigational boundaries like **pages,
+routes, or [modals](https://www.appcues.com/blog/modal-dialog-windows)**.
+
+
+
+AsyncBoundary's [error fallback](../api/AsyncBoundary.md#errorcomponent) and [loading fallback](../api/AsyncBoundary.md#fallback) can both
+be customized.
+
+### Stateful
+
+You may find cases where it's still useful to use a stateful approach to fallbacks when using React 16 and 17.
+For these cases, or compatibility with some component libraries, [useDLE()](../api/useDLE.md) - [D]ata [L]oading [E]rror - is provided.
+
+
+
+```typescript title="ProfileResource" collapsed
+import { Entity, resource } from '@data-client/rest';
+
+export class Profile extends Entity {
+ id: number | undefined = undefined;
+ avatar = '';
+ fullName = '';
+ bio = '';
+
+ static key = 'Profile';
+}
+
+export const ProfileResource = resource({
+ path: '/profiles/:id',
+ schema: Profile,
+});
+```
+
+```html title="ProfileList.vue"
+
+
+
+
Error {{ error.status }}
+
+
+
+
+
+
{{ profile.fullName }}
+
{{ profile.bio }}
+
+
+
+
+```
+
+
+
+Since [useDLE](../api/useDLE.md) does not [useSuspense](../api/useSuspense.md), you won't be able to easily centrally
+orchestrate loading and error code.
+
+## Conditional
+
+
+
+## Subscriptions
+
+When data is likely to change due to external factor; [useSubscription()](../api/useSubscription.md)
+ensures continual updates while a component is mounted. [useLive()](../api/useLive.md) calls both
+[useSubscription()](../api/useSubscription.md) and [useSuspense()](../api/useSuspense.md), making it quite
+easy to use fresh data.
+
+
+
+Subscriptions are orchestrated by [Managers](../api/Manager.md). Out of the box,
+polling based subscriptions can be used by adding [pollFrequency](/rest/api/Endpoint#pollfrequency) to an Endpoint or Resource.
+For pushed based networking protocols like SSE and websockets, see the [example stream manager](../concepts/managers.md#data-stream).
+
+```typescript
+export const getTicker = new RestEndpoint({
+ urlPrefix: 'https://api.exchange.coinbase.com',
+ path: '/products/:productId/ticker',
+ schema: Ticker,
+ // highlight-next-line
+ pollFrequency: 2000,
+});
+```
+
diff --git a/docs/core/getting-started/mutations.md b/docs/core/getting-started/mutations.md
index 44f881adfc18..22b9dbcc68fc 100644
--- a/docs/core/getting-started/mutations.md
+++ b/docs/core/getting-started/mutations.md
@@ -4,7 +4,6 @@ sidebar_label: Mutate Data
description: Safe and high performance data mutations without refetching or writing state management.
---
-import ProtocolTabs from '@site/src/components/ProtocolTabs';
import HooksPlayground from '@site/src/components/HooksPlayground';
import { TodoResource } from '@site/src/components/Demo/code/todo-app/rest/resources';
import { todoFixtures } from '@site/src/fixtures/todos';
diff --git a/docs/core/getting-started/mutations.vue.md b/docs/core/getting-started/mutations.vue.md
new file mode 100644
index 000000000000..55d1f2d7cc0f
--- /dev/null
+++ b/docs/core/getting-started/mutations.vue.md
@@ -0,0 +1,155 @@
+---
+title: Mutating Asynchronous Data in Vue
+sidebar_label: Mutate Data
+description: Safe and high performance data mutations without refetching or writing state management.
+---
+
+import TypeScriptEditor from '@site/src/components/TypeScriptEditor';
+import { TodoResource } from '@site/src/components/Demo/code/todo-app/rest/resources';
+import { todoFixtures } from '@site/src/fixtures/todos';
+import { RestEndpoint } from '@data-client/rest';
+import UseLoading from '../shared/\_useLoading.vue.mdx';
+import VoteDemo from '../shared/\_VoteDemo.vue.mdx';
+
+
+
+
+
+# Data mutations
+
+Using our [Create, Update, and Delete](/docs/concepts/atomic-mutations) endpoints with
+[Controller.fetch()](../api/Controller.md#fetch) reactively updates _all_ appropriate components atomically (at the same time).
+
+[useController()](../api/useController.md) gives components access to this global supercharged [setState()](https://react.dev/reference/react/useState#setstate).
+
+[//]: # 'TODO: Add create, and delete examples as well (in tabs)'
+
+
+
+```ts title="TodoResource" collapsed
+import { Entity, resource } from '@data-client/rest';
+
+export class Todo extends Entity {
+ id = 0;
+ userId = 0;
+ title = '';
+ completed = false;
+
+ static key = 'Todo';
+}
+export const TodoResource = resource({
+ urlPrefix: 'https://jsonplaceholder.typicode.com',
+ path: '/todos/:id',
+ searchParams: {} as { userId?: string | number } | undefined,
+ schema: Todo,
+ optimistic: true,
+});
+```
+
+```html title="TodoItem.vue" {8-12,14-16}
+
+
+
+
+
+```
+
+
+
+Rather than triggering invalidation cascades or using manually written update functions,
+Data Client reactively updates appropriate components using the fetch response.
+
+## Optimistic mutations based on previous state {#optimistic-updates}
+
+
+
+[getOptimisticResponse](/rest/guides/optimistic-updates) is just like [setState with an updater function](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state). [Snapshot](../api/Snapshot.md) provides typesafe access to the previous store value,
+which we use to return the _expected_ fetch response.
+
+Reactive Data Client ensures [data integrity against any possible networking failure or race condition](/rest/guides/optimistic-updates#optimistic-transforms), so don't
+worry about network failures, multiple mutation calls editing the same data, or other common
+problems in asynchronous programming.
+
+## Tracking mutation loading
+
+[useLoading()](../api/useLoading.md) enhances async functions by tracking their loading and error states.
+
+
+
diff --git a/docs/core/shared/_AsyncBoundary.vue.mdx b/docs/core/shared/_AsyncBoundary.vue.mdx
new file mode 100644
index 000000000000..9a784459ae17
--- /dev/null
+++ b/docs/core/shared/_AsyncBoundary.vue.mdx
@@ -0,0 +1,34 @@
+Vue has built-in [Suspense](https://vuejs.org/guide/built-ins/suspense.html) for async components. Pair it with an error boundary for complete async handling:
+
+```html title="Dashboard.vue"
+
+
+
+
+
Dashboard
+
+
Error: {{ error.message }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
diff --git a/docs/core/shared/_VoteDemo.vue.mdx b/docs/core/shared/_VoteDemo.vue.mdx
new file mode 100644
index 000000000000..9181947890b4
--- /dev/null
+++ b/docs/core/shared/_VoteDemo.vue.mdx
@@ -0,0 +1,130 @@
+import TypeScriptEditor from '@site/src/components/TypeScriptEditor';
+
+
+
+```ts title="Post" collapsed
+import { Entity, schema } from '@data-client/rest';
+
+export class Post extends Entity {
+ id = 0;
+ author = { id: 0 };
+ title = '';
+ body = '';
+ votes = 0;
+
+ static key = 'Post';
+
+ static schema = {
+ author: EntityMixin(
+ class User {
+ id = 0;
+ },
+ ),
+ };
+
+ get img() {
+ return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`;
+ }
+}
+```
+
+```ts title="PostResource" {15-22}
+import { resource } from '@data-client/rest';
+import { Post } from './Post';
+
+export { Post };
+
+export const PostResource = resource({
+ path: '/posts/:id',
+ searchParams: {} as { userId?: string | number } | undefined,
+ schema: Post,
+}).extend('vote', {
+ path: '/posts/:id/vote',
+ method: 'POST',
+ body: undefined,
+ schema: Post,
+ getOptimisticResponse(snapshot, { id }) {
+ const post = snapshot.get(Post, { id });
+ if (!post) throw snapshot.abort;
+ return {
+ id,
+ votes: post.votes + 1,
+ };
+ },
+});
+```
+
+```html title="PostItem.vue" collapsed
+
+
+
+