From 39883b60bcbf45074fcd648009462d9b84c592bf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 15:47:34 +0000 Subject: [PATCH 1/3] docs: Make the Managers page's toast and refresh-on-focus examples live Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01C8xTb3f4ys9x7gfbwPacsx --- .../references/managers.md | 144 ++++++++++++++- .../references/managers.vue.md | 148 ++++++++++++++- docs/core/concepts/managers.md | 65 +++---- docs/core/shared/_RefreshManagerDemo.mdx | 166 +++++++++++++++++ docs/core/shared/_ToastManagerDemo.mdx | 169 ++++++++++++++++++ 5 files changed, 645 insertions(+), 47 deletions(-) create mode 100644 docs/core/shared/_RefreshManagerDemo.mdx create mode 100644 docs/core/shared/_ToastManagerDemo.mdx diff --git a/.agents/skills/data-client-manager/references/managers.md b/.agents/skills/data-client-manager/references/managers.md index 2b65f27766b4..c53a1e19100a 100644 --- a/.agents/skills/data-client-manager/references/managers.md +++ b/.agents/skills/data-client-manager/references/managers.md @@ -110,14 +110,31 @@ export default class MetricsManager implements Manager { Show a toast when any [mutation](https://dataclient.io/rest/guides/side-effects) succeeds or fails. -```typescript +```ts title="TodoResource" +import { Entity, resource } from '@data-client/rest'; + +export class Todo extends Entity { + id = 0; + title = ''; + completed = false; + + static key = 'Todo'; +} +export const TodoResource = resource({ + path: '/todos/:id', + schema: Todo, + optimistic: true, +}); +``` + +```ts title="ToastManager" import { type Manager, type Middleware, actionTypes, } from '@data-client/react'; -import { toast } from './toast'; +// toast is the app's toast library, e.g. import { toast } from 'sonner' export default class ToastManager implements Manager { middleware: Middleware = controller => next => async action => { if ( @@ -134,13 +151,88 @@ export default class ToastManager implements Manager { } ``` +```ts title="getManagers" +import { getDefaultManagers } from '@data-client/react'; +import ToastManager from './ToastManager'; + +// the app's managers (DataProvider or DataClientPlugin) +export default function getManagers() { + return [new ToastManager(), ...getDefaultManagers()]; +} +``` + +```tsx title="TodoList" +import { useController, useSuspense } from '@data-client/react'; +import { TodoResource } from './TodoResource'; + +function TodoList() { + const ctrl = useController(); + const todos = useSuspense(TodoResource.getList); + return ( +
+ {todos.map(todo => ( +
+ +
+ ))} +
+ ); +} +render( + <> +
+

Launch tasks

+
+ + + , +); +``` + +The server rejects changes to the archived task, so checking it off rolls back with an error toast. + ### Refresh on focus or reconnect {#refresh-on-focus} [Controller.expireAll()](./Controller.md#expireAll) marks data as [Stale](https://dataclient.io/docs/concepts/expiry-policy#stale), triggering refetch of any _actively rendered_ data without suspending ([stale-while-revalidate](https://dataclient.io/docs/concepts/expiry-policy)). [init()](./Manager.md#init) and [cleanup()](./Manager.md#cleanup) manage the event listeners. -```typescript +```ts title="getRate" +import { Entity, RestEndpoint } from '@data-client/rest'; + +export class Rate extends Entity { + currency = ''; + usd = 0; + fetchedAt = ''; + + pk() { + return this.currency; + } + static key = 'Rate'; +} +export const getRate = new RestEndpoint({ + path: '/rates/:currency', + schema: Rate, +}); +``` + +```ts title="RefreshManager" import type { Manager, Middleware, Controller } from '@data-client/react'; export default class RefreshManager implements Manager { @@ -165,6 +257,52 @@ export default class RefreshManager implements Manager { } ``` +```ts title="getManagers" +import { getDefaultManagers } from '@data-client/react'; +import RefreshManager from './RefreshManager'; + +// the app's managers (DataProvider or DataClientPlugin) +export default function getManagers() { + return [new RefreshManager(), ...getDefaultManagers()]; +} +``` + +```tsx title="Rates" +import { useSuspense } from '@data-client/react'; +import NumberFlow from '@number-flow/react'; +import { getRate } from './getRate'; + +function RateRow({ currency }: { currency: string }) { + const rate = useSuspense(getRate, { currency }); + return ( +
+ {currency} + + + fetched {new Date(rate.fetchedAt).toLocaleTimeString()} + +
+ ); +} +render( + <> +
+

Rates

+
+
+ + +
+ , +); +``` + +Each time the window regains focus or comes back online, every rate refetches and its fetch time +updates, without a loading state. + ### Cross-tab synchronization {#cross-tab-sync} When a mutation succeeds in one tab, mark data stale in all other tabs using diff --git a/.agents/skills/data-client-manager/references/managers.vue.md b/.agents/skills/data-client-manager/references/managers.vue.md index 79f618194cc8..e1ff64c1baa7 100644 --- a/.agents/skills/data-client-manager/references/managers.vue.md +++ b/.agents/skills/data-client-manager/references/managers.vue.md @@ -110,14 +110,31 @@ export default class MetricsManager implements Manager { Show a toast when any [mutation](https://dataclient.io/rest/guides/side-effects) succeeds or fails. -```typescript +```ts title="TodoResource" +import { Entity, resource } from '@data-client/rest'; + +export class Todo extends Entity { + id = 0; + title = ''; + completed = false; + + static key = 'Todo'; +} +export const TodoResource = resource({ + path: '/todos/:id', + schema: Todo, + optimistic: true, +}); +``` + +```ts title="ToastManager" import { type Manager, type Middleware, actionTypes, } from '@data-client/vue'; -import { toast } from './toast'; +// toast is the app's toast library, e.g. import { toast } from 'sonner' export default class ToastManager implements Manager { middleware: Middleware = controller => next => async action => { if ( @@ -134,13 +151,83 @@ export default class ToastManager implements Manager { } ``` +```ts title="getManagers" +import { getDefaultManagers } from '@data-client/vue'; +import ToastManager from './ToastManager'; + +// the app's managers (DataProvider or DataClientPlugin) +export default function getManagers() { + return [new ToastManager(), ...getDefaultManagers()]; +} +``` + +```html title="TodoList.vue" + + + +``` + +The server rejects changes to the archived task, so checking it off rolls back with an error toast. + ### Refresh on focus or reconnect {#refresh-on-focus} [Controller.expireAll()](./Controller.vue.md#expireAll) marks data as [Stale](https://dataclient.io/vue/concepts/expiry-policy#stale), triggering refetch of any _actively rendered_ data without suspending ([stale-while-revalidate](https://dataclient.io/vue/concepts/expiry-policy)). [init()](./Manager.vue.md#init) and [cleanup()](./Manager.vue.md#cleanup) manage the event listeners. -```typescript +```ts title="getRate" +import { Entity, RestEndpoint } from '@data-client/rest'; + +export class Rate extends Entity { + currency = ''; + usd = 0; + fetchedAt = ''; + + pk() { + return this.currency; + } + static key = 'Rate'; +} +export const getRate = new RestEndpoint({ + path: '/rates/:currency', + schema: Rate, +}); +``` + +```ts title="RefreshManager" import type { Manager, Middleware, Controller } from '@data-client/vue'; export default class RefreshManager implements Manager { @@ -165,6 +252,61 @@ export default class RefreshManager implements Manager { } ``` +```ts title="getManagers" +import { getDefaultManagers } from '@data-client/vue'; +import RefreshManager from './RefreshManager'; + +// the app's managers (DataProvider or DataClientPlugin) +export default function getManagers() { + return [new RefreshManager(), ...getDefaultManagers()]; +} +``` + +```html title="RateRow.vue" + + + +``` + +```html title="Rates.vue" + + + +``` + +Each time the window regains focus or comes back online, every rate refetches and its fetch time +updates, without a loading state. + ### Cross-tab synchronization {#cross-tab-sync} When a mutation succeeds in one tab, mark data stale in all other tabs using diff --git a/docs/core/concepts/managers.md b/docs/core/concepts/managers.md index e23ba140ecf5..18e48f9d6b58 100644 --- a/docs/core/concepts/managers.md +++ b/docs/core/concepts/managers.md @@ -11,6 +11,9 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; import StackBlitz from '@site/src/components/StackBlitz'; import BatchSetDemo from '../shared/\_BatchSetDemo.mdx'; import StreamManagerDemo from '../shared/\_StreamManagerDemo.mdx'; +import ToastManagerDemo from '../shared/\_ToastManagerDemo.mdx'; +import RefreshManagerDemo from '../shared/\_RefreshManagerDemo.mdx'; +import SiteOnly from '@site/src/components/SiteOnly'; @@ -167,29 +170,19 @@ export default class MetricsManager implements Manager { Show a toast when any [mutation](/rest/guides/side-effects) succeeds or fails. -```typescript framework-imports -import { - type Manager, - type Middleware, - actionTypes, -} from '@data-client/react'; -import { toast } from './toast'; + -export default class ToastManager implements Manager { - middleware: Middleware = controller => next => async action => { - if ( - action.type === actionTypes.SET_RESPONSE && - action.endpoint.sideEffect - ) { - if (action.error) toast.error(`${action.endpoint.name} failed`); - else toast.success(`${action.endpoint.name} succeeded`); - } - return next(action); - }; +The server rejects changes to the archived task, so checking it off rolls back with an error toast. - cleanup() {} -} -``` + + +:::react + +Check off a task to try it. Edit the messages in `ToastManager` and the preview runs your version. + +::: + + ### Refresh on focus or reconnect {#refresh-on-focus} @@ -197,30 +190,20 @@ export default class ToastManager implements Manager { triggering refetch of any _actively rendered_ data without suspending ([stale-while-revalidate](./expiry-policy.md)). [init()](../api/Manager.md#init) and [cleanup()](../api/Manager.md#cleanup) manage the event listeners. -```typescript framework-imports -import type { Manager, Middleware, Controller } from '@data-client/react'; + -export default class RefreshManager implements Manager { - declare protected controller: Controller; - protected handle = () => - this.controller.expireAll({ testKey: () => true }); +Each time the window regains focus or comes back online, every rate refetches and its fetch time +updates, without a loading state. - middleware: Middleware = controller => { - this.controller = controller; - return next => async action => next(action); - }; + - init() { - window.addEventListener('focus', this.handle); - window.addEventListener('online', this.handle); - } +:::react - cleanup() { - window.removeEventListener('focus', this.handle); - window.removeEventListener('online', this.handle); - } -} -``` +Switch to another tab or app and back to try it. + +::: + + ### Cross-tab synchronization {#cross-tab-sync} diff --git a/docs/core/shared/_RefreshManagerDemo.mdx b/docs/core/shared/_RefreshManagerDemo.mdx new file mode 100644 index 000000000000..12359110e62d --- /dev/null +++ b/docs/core/shared/_RefreshManagerDemo.mdx @@ -0,0 +1,166 @@ +import FrameworkPlayground from '@site/src/components/FrameworkPlayground'; +import { RestEndpoint } from '@data-client/rest'; + +export const rateFixtures = [ + { + endpoint: new RestEndpoint({ path: '/rates/:currency' }), + response({ currency }) { + this.rates[currency] *= 1 + (Math.random() - 0.5) / 50; + return { + currency, + usd: Math.round(this.rates[currency] * 100) / 100, + fetchedAt: new Date().toISOString(), + }; + }, + delay: 300, + }, +]; +export const getInitialRates = () => ({ + rates: { BTC: 64000, ETH: 3100 }, +}); + + + +```ts title="getRate" collapsed +import { Entity, RestEndpoint } from '@data-client/rest'; + +export class Rate extends Entity { + currency = ''; + usd = 0; + fetchedAt = ''; + + pk() { + return this.currency; + } + static key = 'Rate'; +} +export const getRate = new RestEndpoint({ + path: '/rates/:currency', + schema: Rate, +}); +``` + +```ts title="RefreshManager" framework-imports +import type { Manager, Middleware, Controller } from '@data-client/react'; + +export default class RefreshManager implements Manager { + declare protected controller: Controller; + protected handle = () => + this.controller.expireAll({ testKey: () => true }); + + middleware: Middleware = controller => { + this.controller = controller; + return next => async action => next(action); + }; + + init() { + window.addEventListener('focus', this.handle); + window.addEventListener('online', this.handle); + } + + cleanup() { + window.removeEventListener('focus', this.handle); + window.removeEventListener('online', this.handle); + } +} +``` + +```ts title="getManagers" collapsed framework-imports +import { getDefaultManagers } from '@data-client/react'; +import RefreshManager from './RefreshManager'; + +// the app's managers (DataProvider or DataClientPlugin) +export default function getManagers() { + return [new RefreshManager(), ...getDefaultManagers()]; +} +``` + +:::react + +```tsx title="Rates" +import { useSuspense } from '@data-client/react'; +import NumberFlow from '@number-flow/react'; +import { getRate } from './getRate'; + +function RateRow({ currency }: { currency: string }) { + const rate = useSuspense(getRate, { currency }); + return ( +
+ {currency} + + + fetched {new Date(rate.fetchedAt).toLocaleTimeString()} + +
+ ); +} +render( + <> +
+

Rates

+
+
+ + +
+ , +); +``` + +::: + +:::vue + +```html title="RateRow.vue" collapsed + + + +``` + +```html title="Rates.vue" + + + +``` + +::: + +
diff --git a/docs/core/shared/_ToastManagerDemo.mdx b/docs/core/shared/_ToastManagerDemo.mdx new file mode 100644 index 000000000000..c6878205dd9e --- /dev/null +++ b/docs/core/shared/_ToastManagerDemo.mdx @@ -0,0 +1,169 @@ +import FrameworkPlayground from '@site/src/components/FrameworkPlayground'; +import { RestEndpoint } from '@data-client/rest'; + +export const todoFixtures = [ + { + endpoint: new RestEndpoint({ path: '/todos' }), + args: [], + response: [ + { id: 1, title: 'Ship the release', completed: false }, + { id: 2, title: 'Write the release notes', completed: true }, + { id: 3, title: 'Plan the launch (archived)', completed: false }, + ], + }, + { + endpoint: new RestEndpoint({ path: '/todos/:id', method: 'PATCH' }), + response({ id }, body) { + if (id === 3) + throw Object.assign(new Error('Archived'), { status: 403 }); + return { id, ...body }; + }, + delay: 400, + }, +]; + + + +```ts title="TodoResource" collapsed +import { Entity, resource } from '@data-client/rest'; + +export class Todo extends Entity { + id = 0; + title = ''; + completed = false; + + static key = 'Todo'; +} +export const TodoResource = resource({ + path: '/todos/:id', + schema: Todo, + optimistic: true, +}); +``` + +```ts title="ToastManager" framework-imports +import { + type Manager, + type Middleware, + actionTypes, +} from '@data-client/react'; + +// toast is the app's toast library, e.g. import { toast } from 'sonner' +export default class ToastManager implements Manager { + middleware: Middleware = controller => next => async action => { + if ( + action.type === actionTypes.SET_RESPONSE && + action.endpoint.sideEffect + ) { + if (action.error) toast.error(`${action.endpoint.name} failed`); + else toast.success(`${action.endpoint.name} succeeded`); + } + return next(action); + }; + + cleanup() {} +} +``` + +```ts title="getManagers" collapsed framework-imports +import { getDefaultManagers } from '@data-client/react'; +import ToastManager from './ToastManager'; + +// the app's managers (DataProvider or DataClientPlugin) +export default function getManagers() { + return [new ToastManager(), ...getDefaultManagers()]; +} +``` + +:::react + +```tsx title="TodoList" +import { useController, useSuspense } from '@data-client/react'; +import { TodoResource } from './TodoResource'; + +function TodoList() { + const ctrl = useController(); + const todos = useSuspense(TodoResource.getList); + return ( +
+ {todos.map(todo => ( +
+ +
+ ))} +
+ ); +} +render( + <> +
+

Launch tasks

+
+ + + , +); +``` + +::: + +:::vue + +```html title="TodoList.vue" + + + +``` + +::: + +
From 40ca863023183d6a52978b86c875b8905a341d62 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 18:17:17 +0000 Subject: [PATCH 2/3] docs: Keep the Managers demos' preview UI out of skill references Marks TodoList, Rates and the Vue UI files docs-only (#4458), so the manager skill reference keeps the resources, managers and getManagers. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01C8xTb3f4ys9x7gfbwPacsx --- .../references/managers.md | 77 ------------------ .../references/managers.vue.md | 81 ------------------- docs/core/shared/_RefreshManagerDemo.mdx | 6 +- docs/core/shared/_ToastManagerDemo.mdx | 4 +- 4 files changed, 5 insertions(+), 163 deletions(-) diff --git a/.agents/skills/data-client-manager/references/managers.md b/.agents/skills/data-client-manager/references/managers.md index c53a1e19100a..866c13420e62 100644 --- a/.agents/skills/data-client-manager/references/managers.md +++ b/.agents/skills/data-client-manager/references/managers.md @@ -161,50 +161,6 @@ export default function getManagers() { } ``` -```tsx title="TodoList" -import { useController, useSuspense } from '@data-client/react'; -import { TodoResource } from './TodoResource'; - -function TodoList() { - const ctrl = useController(); - const todos = useSuspense(TodoResource.getList); - return ( -
- {todos.map(todo => ( -
- -
- ))} -
- ); -} -render( - <> -
-

Launch tasks

-
- - - , -); -``` - The server rejects changes to the archived task, so checking it off rolls back with an error toast. ### Refresh on focus or reconnect {#refresh-on-focus} @@ -267,39 +223,6 @@ export default function getManagers() { } ``` -```tsx title="Rates" -import { useSuspense } from '@data-client/react'; -import NumberFlow from '@number-flow/react'; -import { getRate } from './getRate'; - -function RateRow({ currency }: { currency: string }) { - const rate = useSuspense(getRate, { currency }); - return ( -
- {currency} - - - fetched {new Date(rate.fetchedAt).toLocaleTimeString()} - -
- ); -} -render( - <> -
-

Rates

-
-
- - -
- , -); -``` - Each time the window regains focus or comes back online, every rate refetches and its fetch time updates, without a loading state. diff --git a/.agents/skills/data-client-manager/references/managers.vue.md b/.agents/skills/data-client-manager/references/managers.vue.md index e1ff64c1baa7..de088ea641d4 100644 --- a/.agents/skills/data-client-manager/references/managers.vue.md +++ b/.agents/skills/data-client-manager/references/managers.vue.md @@ -161,45 +161,6 @@ export default function getManagers() { } ``` -```html title="TodoList.vue" - - - -``` - The server rejects changes to the archived task, so checking it off rolls back with an error toast. ### Refresh on focus or reconnect {#refresh-on-focus} @@ -262,48 +223,6 @@ export default function getManagers() { } ``` -```html title="RateRow.vue" - - - -``` - -```html title="Rates.vue" - - - -``` - Each time the window regains focus or comes back online, every rate refetches and its fetch time updates, without a loading state. diff --git a/docs/core/shared/_RefreshManagerDemo.mdx b/docs/core/shared/_RefreshManagerDemo.mdx index 12359110e62d..a1a19e654c79 100644 --- a/docs/core/shared/_RefreshManagerDemo.mdx +++ b/docs/core/shared/_RefreshManagerDemo.mdx @@ -82,7 +82,7 @@ export default function getManagers() { :::react -```tsx title="Rates" +```tsx title="Rates" docs-only import { useSuspense } from '@data-client/react'; import NumberFlow from '@number-flow/react'; import { getRate } from './getRate'; @@ -119,7 +119,7 @@ render( :::vue -```html title="RateRow.vue" collapsed +```html title="RateRow.vue" collapsed docs-only diff --git a/docs/core/shared/_ToastManagerDemo.mdx b/docs/core/shared/_ToastManagerDemo.mdx index c6878205dd9e..3e5c4a2b3e13 100644 --- a/docs/core/shared/_ToastManagerDemo.mdx +++ b/docs/core/shared/_ToastManagerDemo.mdx @@ -77,7 +77,7 @@ export default function getManagers() { :::react -```tsx title="TodoList" +```tsx title="TodoList" docs-only import { useController, useSuspense } from '@data-client/react'; import { TodoResource } from './TodoResource'; @@ -125,7 +125,7 @@ render( :::vue -```html title="TodoList.vue" +```html title="TodoList.vue" docs-only