diff --git a/.agents/skills/data-client-manager/references/managers.md b/.agents/skills/data-client-manager/references/managers.md
index 2b65f27766b4..5e70c2e4a380 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,42 @@ 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()];
+}
+```
+
### 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 +211,16 @@ 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()];
+}
+```
+
### 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..c144b23ac5a1 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,42 @@ 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()];
+}
+```
+
### 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 +211,16 @@ 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()];
+}
+```
+
### 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..d2a8e4b5acbd 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,18 @@ 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);
- };
+
- cleanup() {}
-}
-```
+:::react
+
+The server rejects changes to the archived task, so checking it off rolls back with an error toast.
+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 +189,18 @@ 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 });
+
- middleware: Middleware = controller => {
- this.controller = controller;
- return next => async action => next(action);
- };
+:::react
- init() {
- window.addEventListener('focus', this.handle);
- window.addEventListener('online', this.handle);
- }
+Each time the window regains focus or comes back online, every rate refetches and its fetch time
+updates, without a loading state. Switch to another tab or app and back to try it.
- cleanup() {
- window.removeEventListener('focus', this.handle);
- window.removeEventListener('online', this.handle);
- }
-}
-```
+:::
+
+
### 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..a1a19e654c79
--- /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" docs-only
+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(
+ <>
+
+
+
+
+
+ >,
+);
+```
+
+:::
+
+:::vue
+
+```html title="RateRow.vue" collapsed docs-only
+
+
+
+
+ {{ currency }}
+
+
+ fetched {{ new Date(rate.fetchedAt).toLocaleTimeString() }}
+
+
+
+```
+
+```html title="Rates.vue" docs-only
+
+
+
+
+
+
+
+
+
+```
+
+:::
+
+
diff --git a/docs/core/shared/_ToastManagerDemo.mdx b/docs/core/shared/_ToastManagerDemo.mdx
new file mode 100644
index 000000000000..3e5c4a2b3e13
--- /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" docs-only
+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(
+ <>
+
+
+
+ >,
+);
+```
+
+:::
+
+:::vue
+
+```html title="TodoList.vue" docs-only
+
+
+
+
+
+
+
+
+
+
+
+```
+
+:::
+
+