From d01ab2fbd469d2fffafe6ce8a68b9741e0d3fea5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 20:27:13 +0000 Subject: [PATCH 01/11] docs: Vue examples for Controller and manager API pages Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LfrAWX2uC23Ym1SNjRYvo5 --- docs/core/api/Controller.md | 399 +++++++++++++++++++++++++-- docs/core/api/DevToolsManager.md | 53 ++++ docs/core/api/LogoutManager.md | 29 ++ docs/core/api/Manager.md | 20 ++ docs/core/api/PollingSubscription.md | 30 +- docs/core/api/mockInitialState.md | 48 +++- 6 files changed, 557 insertions(+), 22 deletions(-) diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index a3dcdb58bab0..ad76dcf865f2 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -64,6 +64,8 @@ values={[ ]}> +:::react + ```tsx function CreatePost() { const ctrl = useController(); @@ -80,9 +82,36 @@ function CreatePost() { } ``` +::: + +:::vue + +```html title="CreatePost.vue" + + + +``` + +::: + +:::react + ```tsx function UpdatePost({ id }: { id: string }) { const ctrl = useController(); @@ -99,9 +128,38 @@ function UpdatePost({ id }: { id: string }) { } ``` +::: + +:::vue + +```html title="UpdatePost.vue" + + + +``` + +::: + +:::react + ```tsx function PostListItem({ post }: { post: PostResource }) { const ctrl = useController(); @@ -111,7 +169,7 @@ function PostListItem({ post }: { post: PostResource }) { await ctrl.fetch(PostResource.delete, { id: post.id }); history.push('/'); }, - [ctrl, id], + [ctrl, post.id], ); return ( @@ -123,6 +181,36 @@ function PostListItem({ post }: { post: PostResource }) { } ``` +::: + +:::vue + +```html title="PostListItem.vue" + + + +``` + +::: + @@ -132,8 +220,7 @@ function PostListItem({ post }: { post: PostResource }) { When using schemas, the denormalized value is returned ```ts -// highlight-next-line -import { useController } from '@data-client/react'; +const controller = useController(); const post = await controller.fetch( PostResource.getList.push, @@ -199,11 +286,13 @@ Sets all responses' [expiry status](../concepts/expiry-policy.md) matching `test This is sometimes useful to trigger refresh of only data presently shown when there are many parameterizations in cache. +:::react + ```tsx import { type Controller, useController } from '@data-client/react'; const createTradeHandler = (ctrl: Controller) => async trade => { - await ctrl.fetch(TradeResource.getList.push({ user: user.id }, trade)); + await ctrl.fetch(TradeResource.getList.push, { user: user.id }, trade); // highlight-start ctrl.expireAll(AccountResource.get); ctrl.expireAll(AccountResource.getList); @@ -223,6 +312,34 @@ function CreateTrade({ id }: { id: string }) { } ``` +::: + +:::vue + +```html title="CreateTrade.vue" + + + +``` + +::: + :::tip To reduce load, improve performance, and improve state consistency; it can often be @@ -235,6 +352,8 @@ better to [include mutation sideeffects in the mutation response](/rest/guides/s Forces refetching and suspense on [useSuspense](./useSuspense.md) with the same Endpoint and parameters. +:::react + ```tsx function ArticleName({ id }: { id: string }) { const article = useSuspense(ArticleResource.get, { id }); @@ -242,13 +361,43 @@ function ArticleName({ id }: { id: string }) { return (
-

{article.title}

+

{article.title}

); } ``` +::: + +:::vue + +```html title="ArticleName.vue" + + + +``` + +::: + :::tip To refresh while continuing to display stale data - [Controller.fetch](#fetch). @@ -274,6 +423,8 @@ controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' }); [Invalidates](../concepts/expiry-policy#invalid) all [endpoint keys](/rest/api/RestEndpoint#key) matching `testKey`. +:::react + ```tsx function ArticleName({ id }: { id: string }) { const article = useSuspense(ArticleResource.get, { id }); @@ -281,13 +432,43 @@ function ArticleName({ id }: { id: string }) { return (
-

{article.title}

+

{article.title}

); } ``` +::: + +:::vue + +```html title="ArticleName.vue" + + + +``` + +::: + :::tip To refresh while continuing to display stale data - [Controller.expireAll](#expireAll) instead. @@ -296,7 +477,7 @@ To refresh while continuing to display stale data - [Controller.expireAll](#expi Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache. -```tsx +```ts const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); @@ -309,7 +490,9 @@ function useLogout() { It's usually a good idea to also clear cache on 401 (unauthorized) with [LogoutManager](./LogoutManager.md) as well. -```ts +:::react + +```tsx import { DataProvider, LogoutManager, getDefaultManagers } from '@data-client/react'; import ReactDOM from 'react-dom'; import { unAuth } from '../authentication'; @@ -335,12 +518,47 @@ ReactDOM.createRoot(document.body).render( ); ``` +::: + +:::vue + +```ts title="main.ts" +import { createApp } from 'vue'; +import { + DataClientPlugin, + LogoutManager, + getDefaultManagers, +} from '@data-client/vue'; +import { unAuth } from '../authentication'; +import App from './App.vue'; + +const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); + +const managers = [ + new LogoutManager({ + handleLogout(controller) { + // call custom unAuth function we defined + unAuth(); + // still reset the store + controller.invalidateAll({ testKey }); + }, + }), + ...getDefaultManagers(), +]; + +createApp(App).use(DataClientPlugin, { managers }).mount('#app'); +``` + +::: + ### resetEntireStore() {#resetEntireStore} Resets/clears the entire Reactive Data Client cache. All inflight requests will not resolve. This is typically used when logging out or changing authenticated users. +:::react + ```tsx const USER_NUMBER_ONE: string = "1111"; @@ -356,13 +574,45 @@ function UserName() { }, [ctrl]); return (
-

{user.name}

+

{user.name}

); } ``` +::: + +:::vue + +```html title="UserName.vue" + + + +``` + +::: + ### set(queryable, ...args, value) {#set} Updates any [Queryable](/rest/api/schema#queryable) [Schema](/rest/api/schema#schema-overview). @@ -392,6 +642,8 @@ Any components suspending for the given [Endpoint](/rest/api/Endpoint) and args If data already exists for the given [Endpoint](/rest/api/Endpoint) and args, it will be updated. +:::react + ```tsx const ctrl = useController(); @@ -409,7 +661,31 @@ useEffect(() => { }); ``` -This shows a proof of concept in React; however a [Manager websockets implementation](../concepts/managers.md#data-stream) +::: + +:::vue + +```ts +const ctrl = useController(); +let websocket: WebSocket; + +onMounted(() => { + websocket = new WebSocket(url); + + websocket.onmessage = event => + ctrl.setResponse( + EndpointLookup[event.endpoint], + ...event.args, + event.data, + ); +}); + +onUnmounted(() => websocket.close()); +``` + +::: + +This shows a proof of concept in :react[React]:vue[Vue]; however a [Manager websockets implementation](../concepts/managers.md#data-stream) would be much more robust. ### setError(endpoint, ...args, error) {#setError} @@ -432,7 +708,9 @@ Marks a new subscription to a given [Endpoint](/rest/api/Endpoint). This should [useSubscription](./useSubscription.md) and [useLive](./useLive.md) call this on mount. -This might be useful for custom hooks to sub/unsub based on other factors. +This might be useful for custom :react[hooks]:vue[composables] to sub/unsub based on other factors. + +:::react ```tsx const controller = useController(); @@ -444,6 +722,23 @@ useEffect(() => { }, [controller, key]); ``` +::: + +:::vue + +```ts +const controller = useController(); + +// args is a computed() so this re-runs when they change +watchEffect(onCleanup => { + const currentArgs = args.value; + controller.subscribe(endpoint, ...currentArgs); + onCleanup(() => controller.unsubscribe(endpoint, ...currentArgs)); +}); +``` + +::: + ### unsubscribe(endpoint, ...args) {#unsubscribe} Marks completion of subscription to a given [Endpoint](/rest/api/Endpoint). This should @@ -462,27 +757,48 @@ Looks up any [Queryable](/rest/api/schema#queryable) [Schema](/rest/api/schema#s This is used in [useQuery](./useQuery.md) and can be used in [Managers](./Manager.md) to safely access the store. +:::react + ```tsx title="useQuery.ts" import { useController, - useCacheState, + StateContext, type Queryable, type SchemaArgs, type DenormalizeNullable, -} from '@data-client/core'; +} from '@data-client/react'; +import { useContext } from 'react'; /** Oversimplified useQuery */ function useQuery( schema: S, ...args: SchemaArgs ): DenormalizeNullable | undefined { - const state = useCacheState(); + const state = useContext(StateContext); const controller = useController(); return controller.get(schema, ...args, state); } ``` +::: + +:::vue + +In components, [useQuery()](./useQuery.md) keeps the result reactive. In event handlers, pass +[getState()](#getState) to read the latest store: + +```ts +const ctrl = useController(); + +const toggle = (id: string) => { + const todo = ctrl.get(Todo, { id }, ctrl.getState()); + if (todo) ctrl.set(Todo, { id }, { id, completed: !todo.completed }); +}; +``` + +::: + ### getResponse(endpoint, ...args, state) {#getResponse} ```ts title="returns" @@ -533,15 +849,18 @@ A number representing time when it expires. Compare to Date.now(). This is used in [useCache](./useCache.md), [useSuspense](./useSuspense.md) and can be used in [Managers](./Manager.md) to lookup a response with the state provided. +:::react + ```tsx title="useCache.ts" import { useController, StateContext, - EndpointInterface, -} from '@data-client/core'; + type EndpointInterface, +} from '@data-client/react'; +import { useContext } from 'react'; /** Oversimplified useCache */ -function useCache( +function useCache( endpoint: E, ...args: readonly [...Parameters] ) { @@ -551,6 +870,26 @@ function useCache( } ``` +::: + +:::vue + +In event handlers, pass [getState()](#getState) to read the latest store: + +```ts +const ctrl = useController(); + +const handleClick = () => { + const { data, expiryStatus } = ctrl.getResponse( + PostResource.get, + { id: props.id }, + ctrl.getState(), + ); +}; +``` + +::: + ```tsx title="MyManager.ts" import type { Manager, Middleware, actionTypes } from '@data-client/core'; import type { EndpointInterface } from '@data-client/endpoint'; @@ -594,10 +933,12 @@ Gets the internal state of Reactive Data Client that has _already been [committe This should only be used in event handlers or [Managers](./Manager.md). -Using getState() in React's render lifecycle can result in data tearing. +Using getState() in :react[React's render lifecycle can result in data tearing]:vue[a `computed()` or template won't update when the store changes]. :vue[Use [useQuery()](./useQuery.md) or [useCache()](./useCache.md) there instead.] ::: +:::react + ```tsx const controller = useController(); @@ -624,3 +965,25 @@ const updateHandler = useCallback( [id], ); ``` + +::: + +:::vue + +```ts +const controller = useController(); + +const updateHandler = async updatePayload => { + await controller.fetch(MyResource.update, { id: props.id }, updatePayload); + // the store has already been updated when fetch() resolves + const { data: denormalized } = controller.getResponse( + MyResource.update, + { id: props.id }, + updatePayload, + controller.getState(), + ); + router.push(denormalized.getterUrl); +}; +``` + +::: diff --git a/docs/core/api/DevToolsManager.md b/docs/core/api/DevToolsManager.md index a69a9ee56243..fb7d2c97bf41 100644 --- a/docs/core/api/DevToolsManager.md +++ b/docs/core/api/DevToolsManager.md @@ -29,6 +29,8 @@ to send to redux devtools. For example, we can enable the [trace](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md#trace) option to help track down where actions are dispatched from. +:::react + ```tsx title="index.tsx" import { DevToolsManager, @@ -49,6 +51,25 @@ ReactDOM.createRoot(document.body).render( ); ``` +::: + +:::vue + +```ts title="main.ts" +import { createApp } from 'vue'; +import { DataClientPlugin, getDefaultManagers } from '@data-client/vue'; +import App from './App.vue'; + +const managers = getDefaultManagers({ + // highlight-next-line + devToolsManager: { trace: true }, +}); + +createApp(App).use(DataClientPlugin, { managers }).mount('#app'); +``` + +::: + ### skipLogging `(action: ActionTypes) => boolean` @@ -57,6 +78,8 @@ Can skip some actions to be registered in the browser devtool. By default will skip inflight [fetch actions](./Controller.md#fetch) +:::react + ```tsx title="index.tsx" import { DevToolsManager, @@ -84,6 +107,36 @@ ReactDOM.createRoot(document.body).render( ); ``` +::: + +:::vue + +```ts title="main.ts" +import { createApp } from 'vue'; +import { + DevToolsManager, + DataClientPlugin, + getDefaultManagers, +} from '@data-client/vue'; +import App from './App.vue'; + +const managers = + process.env.NODE_ENV !== 'production' + ? [ + // highlight-start + new DevToolsManager(undefined, () => true), + // highlight-end + ...getDefaultManagers().filter( + manager => manager.constructor.name !== 'DevToolsManager', + ), + ] + : getDefaultManagers(); + +createApp(App).use(DataClientPlugin, { managers }).mount('#app'); +``` + +::: + #### Skipping high-frequency updates When using [WebSockets](../concepts/managers.md#data-stream) or other real-time data sources, diff --git a/docs/core/api/LogoutManager.md b/docs/core/api/LogoutManager.md index 7e96a1b06b84..dda915d2642a 100644 --- a/docs/core/api/LogoutManager.md +++ b/docs/core/api/LogoutManager.md @@ -19,6 +19,8 @@ Logs out based on fetch responses. By default this is triggered by [401 (Unautho ## Usage +:::react + +::: + +:::vue + +```ts title="main.ts" +import { createApp } from 'vue'; +import { + DataClientPlugin, + LogoutManager, + getDefaultManagers, +} from '@data-client/vue'; +import App from './App.vue'; + +// highlight-next-line +const managers = [new LogoutManager(), ...getDefaultManagers()]; + +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); +``` + +::: + ### Custom logout handler ```ts @@ -232,6 +257,10 @@ protected shouldLogout(error: UnknownError) { } ``` +:::react + ## Github Example + +::: diff --git a/docs/core/api/Manager.md b/docs/core/api/Manager.md index b69e343a3165..852cf6e713b1 100644 --- a/docs/core/api/Manager.md +++ b/docs/core/api/Manager.md @@ -80,6 +80,8 @@ installed once per app, so managers are created once. ::: +:::react + +::: + +:::vue + +```ts title="main.ts" +import { createApp } from 'vue'; +import { DataClientPlugin, getDefaultManagers } from '@data-client/vue'; +import App from './App.vue'; + +const managers = [...getDefaultManagers(), new MyManager()]; + +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); +``` + +::: + ## Control flow Managers integrate with the DataProvider store with their lifecycles and middleware. They orchestrate complex control diff --git a/docs/core/api/PollingSubscription.md b/docs/core/api/PollingSubscription.md index f33adb0edf32..b8081f6b15c0 100644 --- a/docs/core/api/PollingSubscription.md +++ b/docs/core/api/PollingSubscription.md @@ -16,6 +16,8 @@ resource. ::: +:::react + ```tsx import { SubscriptionManager, @@ -38,10 +40,36 @@ ReactDOM.render( ); ``` +::: + +:::vue + +```ts title="main.ts" +import { createApp } from 'vue'; +import { + SubscriptionManager, + PollingSubscription, + DataClientPlugin, + NetworkManager, +} from '@data-client/vue'; +import App from './App.vue'; + +const managers = [ + new NetworkManager(), + new SubscriptionManager(PollingSubscription), +]; + +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); +``` + +::: + ## Dispatched Actions - 'rdc/fetch' > #### Note: > -> This is already used by `DataProvider` by default. +> This is already used by :react[`DataProvider`]:vue[`DataClientPlugin`] by default. diff --git a/docs/core/api/mockInitialState.md b/docs/core/api/mockInitialState.md index 50d77bdbc6fa..ec53f59d071f 100644 --- a/docs/core/api/mockInitialState.md +++ b/docs/core/api/mockInitialState.md @@ -27,6 +27,8 @@ This can be used as the :react[initialState prop for [<DataProvider /\>](./Da ## Example +:::react + ```typescript import { DataProvider } from '@data-client/react'; import { mockInitialState } from '@data-client/test'; @@ -36,9 +38,9 @@ import MyComponentToTest from 'components/MyComponentToTest'; const results = [ { - request: ArticleResource.getList, - params: { maxResults: 10 }, - result: [ + endpoint: ArticleResource.getList, + args: [{ maxResults: 10 }], + response: [ { id: 5, content: 'have a merry christmas', @@ -59,3 +61,43 @@ const results = [ ; ``` + +::: + +:::vue + +```ts +import { createApp } from 'vue'; +import { DataClientPlugin } from '@data-client/vue'; +import { mockInitialState } from '@data-client/test'; + +import ArticleResource from 'resources/ArticleResource'; +import MyComponentToTest from 'components/MyComponentToTest.vue'; + +const results = [ + { + endpoint: ArticleResource.getList, + args: [{ maxResults: 10 }], + response: [ + { + id: 5, + content: 'have a merry christmas', + author: 2, + contributors: [], + }, + { + id: 532, + content: 'never again', + author: 23, + contributors: [5], + }, + ], + }, +]; + +createApp(MyComponentToTest) + .use(DataClientPlugin, { initialState: mockInitialState(results) }) + .mount('#app'); +``` + +::: From 22250a5b758ca0b3a5ad8b70fb7fbd1e938728df Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 21:49:53 +0000 Subject: [PATCH 02/11] docs: Make Manager.md prose and editor imports framework-neutral Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LfrAWX2uC23Ym1SNjRYvo5 --- docs/core/api/Manager.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/core/api/Manager.md b/docs/core/api/Manager.md index 852cf6e713b1..77bce7efce13 100644 --- a/docs/core/api/Manager.md +++ b/docs/core/api/Manager.md @@ -15,7 +15,7 @@ import TypeScriptEditor from '@site/src/components/TypeScriptEditor'; # Manager -`Managers` are singletons that handle global side-effects. Kind of like [useEffect()](https://react.dev/reference/react/useEffect) for the central data +`Managers` are singletons that handle global side-effects. Kind of like :react[[useEffect()](https://react.dev/reference/react/useEffect)]:vue[[watchEffect()](https://vuejs.org/api/reactivity-core.html#watcheffect)] for the central data store. The default managers orchestrate the complex asynchronous behavior that Data Client @@ -44,9 +44,11 @@ interface Manager { `middleware` is very similar to a [redux middleware](https://redux.js.org/advanced/middleware). The only differences is that the `next()` function returns a `Promise`. This promise resolves when the reducer update is -[committed](https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/#general-algorithm) +:react[[committed](https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/#general-algorithm) when using <DataProvider /\>. This is necessary since the commit phase is asynchronously scheduled. This enables building -managers that perform work after the DOM is updated and also with the newly computed state. +managers that perform work after the DOM is updated and also with the newly computed state.]:vue[committed to the +[DataClientPlugin](../getting-started/installation.md) store. This enables building managers that perform work with the +newly computed state.] Since redux is fully synchronous, an adapter must be placed in front of Reactive Data Client style middleware to ensure they can consume a promise. Conversely, redux middleware must be changed to pass through promises. @@ -239,7 +241,7 @@ app.mount('#app'); ## Control flow -Managers integrate with the DataProvider store with their lifecycles and middleware. They orchestrate complex control +Managers integrate with the :react[DataProvider]:vue[DataClientPlugin] store with their lifecycles and middleware. They orchestrate complex control flows by interfacing via intercepting and dispatching [actions](./Actions.md), as well as reading the internal state. ```ts -import type { Manager, Middleware } from '@data-client/react'; -import { actionTypes } from '@data-client/react'; +import type { Manager, Middleware } from '@data-client/core'; +import { actionTypes } from '@data-client/core'; export default class LoggingManager implements Manager { middleware: Middleware = controller => next => async action => { @@ -308,7 +310,7 @@ export default class LoggingManager implements Manager { console.info( `${action.endpoint.name} ${JSON.stringify(action.response)}`, ); - // wait for state update to be committed to React + // wait for state update to be committed await next(action); // get the data from the store, which may be merged with existing state const { data } = controller.getResponse( @@ -348,8 +350,8 @@ export default function isEntity(schema: Schema): schema is EntityInterface { ```ts title="SubsManager" -import type { Manager, Middleware, EntityInterface } from '@data-client/react'; -import { actionTypes } from '@data-client/react'; +import type { Manager, Middleware, EntityInterface } from '@data-client/core'; +import { actionTypes } from '@data-client/core'; import isEntity from './isEntity'; export default class CustomSubsManager implements Manager { From cb6449383641b050095ca5bf8dda50b06bef948a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 22:00:39 +0000 Subject: [PATCH 03/11] docs: Fix Vue getState example timing claim Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LfrAWX2uC23Ym1SNjRYvo5 --- docs/core/api/Controller.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index ad76dcf865f2..3241c99cf5c9 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -532,6 +532,7 @@ import { import { unAuth } from '../authentication'; import App from './App.vue'; +const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); const managers = [ @@ -973,17 +974,19 @@ const updateHandler = useCallback( ```ts const controller = useController(); -const updateHandler = async updatePayload => { - await controller.fetch(MyResource.update, { id: props.id }, updatePayload); - // the store has already been updated when fetch() resolves - const { data: denormalized } = controller.getResponse( - MyResource.update, +const handleRefresh = () => { + const { expiryStatus } = controller.getResponse( + MyResource.get, { id: props.id }, - updatePayload, controller.getState(), ); - router.push(denormalized.getterUrl); + // only refetch if the data is no longer fresh + if (expiryStatus !== ExpiryStatus.Valid) + controller.fetch(MyResource.get, { id: props.id }); }; ``` +Mutations (`sideEffect: true`) resolve _before_ the store is updated, so read their result from the +value `fetch()` resolves with rather than `getState()`. + ::: From 76ff179d3a3f7a80f24bd2fa4dd17192a0431ce6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 23:32:10 +0000 Subject: [PATCH 04/11] docs: Simplify Vue examples on Controller and manager pages Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LfrAWX2uC23Ym1SNjRYvo5 --- docs/core/api/Controller.md | 38 ++++++++++++++++++------------- docs/core/api/DevToolsManager.md | 24 +++++++++---------- docs/core/api/Manager.md | 20 ++++++++++++---- docs/core/api/mockInitialState.md | 6 ++--- 4 files changed, 52 insertions(+), 36 deletions(-) diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index 3241c99cf5c9..d6a98a901ecb 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -277,8 +277,12 @@ An [example](https://stackblitz.com/github/reactive/data-client/tree/master/exam }, ``` +:::react + +::: + ### expireAll(\{ testKey }) {#expireAll} Sets all responses' [expiry status](../concepts/expiry-policy.md) matching `testKey` to [Stale](../concepts/expiry-policy.md#stale). @@ -547,7 +551,9 @@ const managers = [ ...getDefaultManagers(), ]; -createApp(App).use(DataClientPlugin, { managers }).mount('#app'); +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); ``` ::: @@ -875,19 +881,8 @@ function useCache( :::vue -In event handlers, pass [getState()](#getState) to read the latest store: - -```ts -const ctrl = useController(); - -const handleClick = () => { - const { data, expiryStatus } = ctrl.getResponse( - PostResource.get, - { id: props.id }, - ctrl.getState(), - ); -}; -``` +In event handlers, pass [getState()](#getState) to read the latest store, as in the +[getState() example](#getState). ::: @@ -930,14 +925,25 @@ Returns a [Snapshot](./Snapshot.md). Gets the internal state of Reactive Data Client that has _already been [committed](https://react.dev/learn/render-and-commit#step-3-react-commits-changes-to-the-dom)_. -:::warning +::::warning This should only be used in event handlers or [Managers](./Manager.md). -Using getState() in :react[React's render lifecycle can result in data tearing]:vue[a `computed()` or template won't update when the store changes]. :vue[Use [useQuery()](./useQuery.md) or [useCache()](./useCache.md) there instead.] +:::react + +Using getState() in React's render lifecycle can result in data tearing. + +::: + +:::vue + +Using getState() in a `computed()` or template won't update when the store changes. Use +[useQuery()](./useQuery.md) or [useCache()](./useCache.md) there instead. ::: +:::: + :::react ```tsx diff --git a/docs/core/api/DevToolsManager.md b/docs/core/api/DevToolsManager.md index fb7d2c97bf41..968b9ead732b 100644 --- a/docs/core/api/DevToolsManager.md +++ b/docs/core/api/DevToolsManager.md @@ -65,7 +65,9 @@ const managers = getDefaultManagers({ devToolsManager: { trace: true }, }); -createApp(App).use(DataClientPlugin, { managers }).mount('#app'); +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); ``` ::: @@ -120,19 +122,15 @@ import { } from '@data-client/vue'; import App from './App.vue'; -const managers = - process.env.NODE_ENV !== 'production' - ? [ - // highlight-start - new DevToolsManager(undefined, () => true), - // highlight-end - ...getDefaultManagers().filter( - manager => manager.constructor.name !== 'DevToolsManager', - ), - ] - : getDefaultManagers(); +// production builds leave out DevToolsManager +const managers = getDefaultManagers({ + // highlight-next-line + devToolsManager: new DevToolsManager(undefined, () => true), +}); -createApp(App).use(DataClientPlugin, { managers }).mount('#app'); +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); ``` ::: diff --git a/docs/core/api/Manager.md b/docs/core/api/Manager.md index 77bce7efce13..05011f24ede8 100644 --- a/docs/core/api/Manager.md +++ b/docs/core/api/Manager.md @@ -43,12 +43,24 @@ interface Manager { ### middleware `middleware` is very similar to a [redux middleware](https://redux.js.org/advanced/middleware). -The only differences is that the `next()` function returns a `Promise`. This promise resolves when the reducer update is -:react[[committed](https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/#general-algorithm) +The only differences is that the `next()` function returns a `Promise`. + +:::react + +This promise resolves when the reducer update is +[committed](https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/#general-algorithm) when using <DataProvider /\>. This is necessary since the commit phase is asynchronously scheduled. This enables building -managers that perform work after the DOM is updated and also with the newly computed state.]:vue[committed to the +managers that perform work after the DOM is updated and also with the newly computed state. + +::: + +:::vue + +This promise resolves when the reducer update is committed to the [DataClientPlugin](../getting-started/installation.md) store. This enables building managers that perform work with the -newly computed state.] +newly computed state. + +::: Since redux is fully synchronous, an adapter must be placed in front of Reactive Data Client style middleware to ensure they can consume a promise. Conversely, redux middleware must be changed to pass through promises. diff --git a/docs/core/api/mockInitialState.md b/docs/core/api/mockInitialState.md index ec53f59d071f..78616735cdd2 100644 --- a/docs/core/api/mockInitialState.md +++ b/docs/core/api/mockInitialState.md @@ -95,9 +95,9 @@ const results = [ }, ]; -createApp(MyComponentToTest) - .use(DataClientPlugin, { initialState: mockInitialState(results) }) - .mount('#app'); +const app = createApp(MyComponentToTest); +app.use(DataClientPlugin, { initialState: mockInitialState(results) }); +app.mount('#app'); ``` ::: From c8f88758073fb4b1dd01274b94c0bda8777219ff Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 23:37:26 +0000 Subject: [PATCH 05/11] docs: Vue version of DevToolsManager predicate example Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LfrAWX2uC23Ym1SNjRYvo5 --- docs/core/api/DevToolsManager.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/core/api/DevToolsManager.md b/docs/core/api/DevToolsManager.md index 08d4b1d3ed86..628691fed1db 100644 --- a/docs/core/api/DevToolsManager.md +++ b/docs/core/api/DevToolsManager.md @@ -141,6 +141,8 @@ When using [WebSockets](../concepts/managers.md#data-stream) or other real-time high-frequency updates can overwhelm the DevTools extension. Use the `predicate` option to filter out specific action types or schemas: +:::react + ```tsx title="index.tsx" import { getDefaultManagers, actionTypes } from '@data-client/react'; import { Ticker } from './resources/Ticker'; @@ -160,6 +162,31 @@ const managers = getDefaultManagers({ }); ``` +::: + +:::vue + +```ts title="managers.ts" +import { getDefaultManagers, actionTypes } from '@data-client/vue'; +import { Ticker } from './resources/Ticker'; + +const managers = getDefaultManagers({ + devToolsManager: { + // Increase latency buffer for high-frequency updates + latency: 1000, + // Skip WebSocket SET actions for Ticker to reduce log spam + // (including batched set([Ticker], rows) writes) + // highlight-start + predicate: (state, action) => + action.type !== actionTypes.SET || + (action.schema !== Ticker && action.schema[0] !== Ticker), + // highlight-end + }, +}); +``` + +::: + ## Programmatic store access {#controllers} In development mode, `DevToolsManager` registers each [Controller](/docs/api/Controller) on From 6eafbc058361ce760f947dad457046c53b36dd5f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 23:39:24 +0000 Subject: [PATCH 06/11] docs: Simplify React DevToolsManager skipLogging example; define myDomain Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LfrAWX2uC23Ym1SNjRYvo5 --- docs/core/api/Controller.md | 1 + docs/core/api/DevToolsManager.md | 16 +++++----------- docs/core/api/LogoutManager.md | 1 + 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index 9cd475abd63d..14fb77bea05b 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -502,6 +502,7 @@ import { DataProvider, LogoutManager, getDefaultManagers } from '@data-client/re import ReactDOM from 'react-dom'; import { unAuth } from '../authentication'; +const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); const managers = [ diff --git a/docs/core/api/DevToolsManager.md b/docs/core/api/DevToolsManager.md index 628691fed1db..deba5f0bd6b0 100644 --- a/docs/core/api/DevToolsManager.md +++ b/docs/core/api/DevToolsManager.md @@ -90,17 +90,11 @@ import { } from '@data-client/react'; import ReactDOM from 'react-dom'; -const managers = - process.env.NODE_ENV !== 'production' - ? [ - // highlight-start - new DevToolsManager(undefined, () => true), - // highlight-end - ...getDefaultManagers().filter( - manager => manager.constructor.name !== 'DevToolsManager', - ), - ] - : getDefaultManagers(); +// production builds leave out DevToolsManager +const managers = getDefaultManagers({ + // highlight-next-line + devToolsManager: new DevToolsManager(undefined, () => true), +}); ReactDOM.createRoot(document.body).render( diff --git a/docs/core/api/LogoutManager.md b/docs/core/api/LogoutManager.md index dda915d2642a..250a36894761 100644 --- a/docs/core/api/LogoutManager.md +++ b/docs/core/api/LogoutManager.md @@ -220,6 +220,7 @@ Use [controller.invalidateAll](./Controller.md#invalidateAll) to only clear part ```ts import { unAuth } from '../authentication'; +const myDomain = 'http://test.com'; // highlight-next-line const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); From 57b331f8835d35db69db4e91e1f7fb327a1ea531 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 21:51:35 +0000 Subject: [PATCH 07/11] docs: Vue invalidate refetches without suspending Vue's useSuspense can't re-suspend after setup, so it keeps returning the current data while invalidate()/invalidateAll() refetch. Say so on the Vue page, label the Vue buttons "Refetch", and keep the "display stale data" tips React-only. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LfrAWX2uC23Ym1SNjRYvo5 --- docs/core/api/Controller.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index 1c5f11fe5bf2..98d3d87c035f 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -354,8 +354,9 @@ better to [include mutation sideeffects in the mutation response](/rest/guides/s ### invalidate(endpoint, ...args) {#invalidate} -Forces refetching and suspense on [useSuspense](./useSuspense.md) with the same Endpoint -and parameters. +Forces refetching :react[and suspense ]on [useSuspense](./useSuspense.md) with the same Endpoint +and parameters.:vue[ Vue can't suspend a component again after setup, so `useSuspense()` keeps returning the +current data until the refetch resolves.] :::react @@ -395,7 +396,7 @@ function ArticleName({ id }: { id: string }) {

{{ article.title }}

@@ -403,12 +404,16 @@ function ArticleName({ id }: { id: string }) { ::: +::::react + :::tip To refresh while continuing to display stale data - [Controller.fetch](#fetch). ::: +:::: + :::tip[Invalidate many endpoints at once] Use [schema.Invalidate](/rest/api/Invalidate) to invalidate every endpoint that contains a given entity. @@ -417,7 +422,7 @@ For REST try using [Resource.delete](/rest/api/resource#delete) ```ts // deletes MyResource(5) -// this will resuspend MyResource.get({id: '5'}) +// this will refetch MyResource.get({id: '5'}) // and remove it from MyResource.getList controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' }); ``` @@ -466,7 +471,7 @@ function ArticleName({ id }: { id: string }) {

{{ article.title }}

@@ -474,12 +479,16 @@ function ArticleName({ id }: { id: string }) { ::: +::::react + :::tip To refresh while continuing to display stale data - [Controller.expireAll](#expireAll) instead. ::: +:::: + Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache. ```ts From 6b3023df8b81c3b8b991c2486749d702a59cdbad Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 04:07:02 +0000 Subject: [PATCH 08/11] docs: Use getter args in Vue Controller examples; regenerate skill references Master now resolves getter args in Vue composables and its docs pass `() => ({ id: props.id })` instead of computed(). Match that in the invalidate examples and the subscribe sketch, note in the expiry policy that mounted Vue components keep their data while invalid data refetches, and regenerate skill references from the updated docs. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LfrAWX2uC23Ym1SNjRYvo5 --- .../references/Actions.vue.md | 2 +- .../references/Controller.md | 51 ++- .../references/Controller.vue.md | 378 +++++++++--------- .../references/LogoutManager.md | 1 + .../references/LogoutManager.vue.md | 86 ++++ .../data-client-manager/references/Manager.md | 14 +- .../references/Manager.vue.md | 138 +------ .../references/managers.vue.md | 2 +- .../references/mockInitialState.md | 6 +- .../references/Controller.md | 51 ++- .../references/expiry-policy.md | 3 +- .../references/expiry-policy.vue.md | 5 +- .../data-client-vue/references/Controller.md | 376 +++++++++-------- docs/core/api/Controller.md | 42 +- docs/core/concepts/expiry-policy.md | 5 +- 15 files changed, 589 insertions(+), 571 deletions(-) create mode 100644 .agents/skills/data-client-manager/references/LogoutManager.vue.md diff --git a/.agents/skills/data-client-manager/references/Actions.vue.md b/.agents/skills/data-client-manager/references/Actions.vue.md index bdf040663376..a162d2d3a835 100644 --- a/.agents/skills/data-client-manager/references/Actions.vue.md +++ b/.agents/skills/data-client-manager/references/Actions.vue.md @@ -143,7 +143,7 @@ interface SetResponseAction { Sent by [Controller.setResponse()](./Controller.vue.md#setResponse), [NetworkManager](https://dataclient.io/vue/api/NetworkManager) -Read by [NetworkManager](https://dataclient.io/vue/api/NetworkManager), [LogoutManager](./LogoutManager.md) +Read by [NetworkManager](https://dataclient.io/vue/api/NetworkManager), [LogoutManager](./LogoutManager.vue.md) ## RESET diff --git a/.agents/skills/data-client-manager/references/Controller.md b/.agents/skills/data-client-manager/references/Controller.md index a17432382582..29fe4f0b7687 100644 --- a/.agents/skills/data-client-manager/references/Controller.md +++ b/.agents/skills/data-client-manager/references/Controller.md @@ -91,7 +91,7 @@ function PostListItem({ post }: { post: PostResource }) { await ctrl.fetch(PostResource.delete, { id: post.id }); history.push('/'); }, - [ctrl, id], + [ctrl, post.id], ); return ( @@ -109,7 +109,7 @@ function PostListItem({ post }: { post: PostResource }) { > When using schemas, the denormalized value is returned > > ```ts -> import { useController } from '@data-client/react'; +> const controller = useController(); > > const post = await controller.fetch( > PostResource.getList.push, @@ -175,7 +175,7 @@ when there are many parameterizations in cache. import { type Controller, useController } from '@data-client/react'; const createTradeHandler = (ctrl: Controller) => async trade => { - await ctrl.fetch(TradeResource.getList.push({ user: user.id }, trade)); + await ctrl.fetch(TradeResource.getList.push, { user: user.id }, trade); ctrl.expireAll(AccountResource.get); ctrl.expireAll(AccountResource.getList); }; @@ -200,7 +200,7 @@ function CreateTrade({ id }: { id: string }) { ### invalidate(endpoint, ...args) {#invalidate} -Forces refetching and suspense on [useSuspense](https://dataclient.io/docs/api/useSuspense) with the same Endpoint +Forces refetching and suspenseon [useSuspense](https://dataclient.io/docs/api/useSuspense) with the same Endpoint and parameters. ```tsx @@ -210,8 +210,10 @@ function ArticleName({ id }: { id: string }) { return (
-

{article.title}

- +

{article.title}

+
); } @@ -229,7 +231,7 @@ function ArticleName({ id }: { id: string }) { > > ```ts > // deletes MyResource(5) -> // this will resuspend MyResource.get({id: '5'}) +> // this will refetch MyResource.get({id: '5'}) > // and remove it from MyResource.getList > controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' }); > ``` @@ -245,8 +247,10 @@ function ArticleName({ id }: { id: string }) { return (
-

{article.title}

- +

{article.title}

+
); } @@ -258,7 +262,7 @@ function ArticleName({ id }: { id: string }) { Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache. -```tsx +```ts const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); @@ -271,11 +275,16 @@ function useLogout() { It's usually a good idea to also clear cache on 401 (unauthorized) with [LogoutManager](./LogoutManager.md) as well. -```ts -import { DataProvider, LogoutManager, getDefaultManagers } from '@data-client/react'; +```tsx +import { + DataProvider, + LogoutManager, + getDefaultManagers, +} from '@data-client/react'; import { createRoot } from 'react-dom/client'; import { unAuth } from '../authentication'; +const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); const managers = [ @@ -304,7 +313,7 @@ Resets/clears the entire Reactive Data Client cache. All inflight requests will This is typically used when logging out or changing authenticated users. ```tsx -const USER_NUMBER_ONE: string = "1111"; +const USER_NUMBER_ONE: string = '1111'; function UserName() { const user = useSuspense(CurrentUserResource.get); @@ -317,7 +326,7 @@ function UserName() { }, [ctrl]); return (
-

{user.name}

+

{user.name}

); @@ -494,18 +503,19 @@ This is used in [useQuery](https://dataclient.io/docs/api/useQuery) and can be u ```tsx title="useQuery.ts" import { useController, - useCacheState, + StateContext, type Queryable, type SchemaArgs, type DenormalizeNullable, -} from '@data-client/core'; +} from '@data-client/react'; +import { useContext } from 'react'; /** Oversimplified useQuery */ function useQuery( schema: S, ...args: SchemaArgs ): DenormalizeNullable | undefined { - const state = useCacheState(); + const state = useContext(StateContext); const controller = useController(); return controller.get(schema, ...args, state); @@ -566,11 +576,12 @@ This is used in [useCache](https://dataclient.io/docs/api/useCache), [useSuspens import { useController, StateContext, - EndpointInterface, -} from '@data-client/core'; + type EndpointInterface, +} from '@data-client/react'; +import { useContext } from 'react'; /** Oversimplified useCache */ -function useCache( +function useCache( endpoint: E, ...args: readonly [...Parameters] ) { diff --git a/.agents/skills/data-client-manager/references/Controller.vue.md b/.agents/skills/data-client-manager/references/Controller.vue.md index 7202b5440324..8863a64e7824 100644 --- a/.agents/skills/data-client-manager/references/Controller.vue.md +++ b/.agents/skills/data-client-manager/references/Controller.vue.md @@ -46,61 +46,72 @@ the response or error upon completion. **Create** -```tsx -function CreatePost() { +```html title="CreatePost.vue" + + + ``` **Update** -```tsx -function UpdatePost({ id }: { id: string }) { +```html title="UpdatePost.vue" + + + ``` **Delete** -```tsx -function PostListItem({ post }: { post: PostResource }) { - const ctrl = useController(); +```html title="PostListItem.vue" + + + ``` > **Tip** @@ -109,7 +120,7 @@ function PostListItem({ post }: { post: PostResource }) { > When using schemas, the denormalized value is returned > > ```ts -> import { useController } from '@data-client/react'; +> const controller = useController(); > > const post = await controller.fetch( > PostResource.getList.push, @@ -162,8 +173,6 @@ An [example](https://stackblitz.com/github/reactive/data-client/tree/master/exam }, ``` -Example app: [github-app](https://github.com/reactive/data-client/tree/master/examples/github-app) ([`src/routing/routes.tsx`](https://github.com/reactive/data-client/blob/master/examples/github-app/src/routing/routes.tsx)) - ### expireAll({ testKey }) {#expireAll} Sets all responses' [expiry status](https://dataclient.io/vue/concepts/expiry-policy) matching `testKey` to [Stale](https://dataclient.io/vue/concepts/expiry-policy#stale). @@ -171,26 +180,28 @@ Sets all responses' [expiry status](https://dataclient.io/vue/concepts/expiry-po This is sometimes useful to trigger refresh of only data presently shown when there are many parameterizations in cache. -```tsx -import { type Controller, useController } from '@data-client/react'; +```html title="CreateTrade.vue" + - return ( -
- - - - - ); -} + ``` > **Tip** @@ -200,27 +211,32 @@ function CreateTrade({ id }: { id: string }) { ### invalidate(endpoint, ...args) {#invalidate} -Forces refetching and suspense on [useSuspense](https://dataclient.io/vue/api/useSuspense) with the same Endpoint -and parameters. +Forces refetching on [useSuspense](https://dataclient.io/vue/api/useSuspense) with the same Endpoint +and parameters. Vue can't suspend a component again after setup, so `useSuspense()` keeps returning the +current data until the refetch resolves. -```tsx -function ArticleName({ id }: { id: string }) { - const article = useSuspense(ArticleResource.get, { id }); - const ctrl = useController(); +```html title="ArticleName.vue" + + + ``` -> **Tip** -> -> To refresh while continuing to display stale data - [Controller.fetch](#fetch). - > **Tip: Invalidate many endpoints at once** > > Use [schema.Invalidate](https://dataclient.io/rest/api/Invalidate) to invalidate every endpoint that contains a given entity. @@ -229,7 +245,7 @@ function ArticleName({ id }: { id: string }) { > > ```ts > // deletes MyResource(5) -> // this will resuspend MyResource.get({id: '5'}) +> // this will refetch MyResource.get({id: '5'}) > // and remove it from MyResource.getList > controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' }); > ``` @@ -238,27 +254,31 @@ function ArticleName({ id }: { id: string }) { [Invalidates](https://dataclient.io/vue/concepts/expiry-policy#invalid) all [endpoint keys](https://dataclient.io/rest/api/RestEndpoint#key) matching `testKey`. -```tsx -function ArticleName({ id }: { id: string }) { - const article = useSuspense(ArticleResource.get, { id }); - const ctrl = useController(); +```html title="ArticleName.vue" + + + ``` -> **Tip** -> -> To refresh while continuing to display stale data - [Controller.expireAll](#expireAll) instead. - Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache. -```tsx +```ts const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); @@ -268,14 +288,20 @@ function useLogout() { } ``` -It's usually a good idea to also clear cache on 401 (unauthorized) with [LogoutManager](./LogoutManager.md) +It's usually a good idea to also clear cache on 401 (unauthorized) with [LogoutManager](./LogoutManager.vue.md) as well. -```ts -import { DataProvider, LogoutManager, getDefaultManagers } from '@data-client/react'; -import { createRoot } from 'react-dom/client'; +```ts title="main.ts" +import { createApp } from 'vue'; +import { + DataClientPlugin, + LogoutManager, + getDefaultManagers, +} from '@data-client/vue'; import { unAuth } from '../authentication'; +import App from './App.vue'; +const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); const managers = [ @@ -290,11 +316,9 @@ const managers = [ ...getDefaultManagers(), ]; -createRoot(document.body).render( - - - , -); +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); ``` ### resetEntireStore() {#resetEntireStore} @@ -303,25 +327,29 @@ Resets/clears the entire Reactive Data Client cache. All inflight requests will This is typically used when logging out or changing authenticated users. -```tsx -const USER_NUMBER_ONE: string = "1111"; +```html title="UserName.vue" + + + ``` ### set(queryable, ...args, value) {#set} @@ -421,11 +449,12 @@ Any components suspending for the given [Endpoint](https://dataclient.io/rest/ap If data already exists for the given [Endpoint](https://dataclient.io/rest/api/Endpoint) and args, it will be updated. -```tsx +```ts const ctrl = useController(); +let websocket: WebSocket; -useEffect(() => { - const websocket = new Websocket(url); +onMounted(() => { + websocket = new WebSocket(url); websocket.onmessage = event => ctrl.setResponse( @@ -433,12 +462,12 @@ useEffect(() => { ...event.args, event.data, ); - - return () => websocket.close(); }); + +onUnmounted(() => websocket.close()); ``` -This shows a proof of concept in React; however a [Manager websockets implementation](./managers.vue.md#data-stream) +This shows a proof of concept in Vue; however a [Manager websockets implementation](./managers.vue.md#data-stream) would be much more robust. ### setError(endpoint, ...args, error) {#setError} @@ -461,16 +490,17 @@ Marks a new subscription to a given [Endpoint](https://dataclient.io/rest/api/En [useSubscription](https://dataclient.io/vue/api/useSubscription) and [useLive](https://dataclient.io/vue/api/useLive) call this on mount. -This might be useful for custom hooks to sub/unsub based on other factors. +This might be useful for custom composables to sub/unsub based on other factors. -```tsx +```ts const controller = useController(); -const key = endpoint.key(...args); -useEffect(() => { - controller.subscribe(endpoint, ...args); - return () => controller.unsubscribe(endpoint, ...args); -}, [controller, key]); +// args can be a ref, computed or getter; this re-runs when it changes +watchEffect(onCleanup => { + const currentArgs = toValue(args); + controller.subscribe(endpoint, ...currentArgs); + onCleanup(() => controller.unsubscribe(endpoint, ...currentArgs)); +}); ``` ### unsubscribe(endpoint, ...args) {#unsubscribe} @@ -491,25 +521,16 @@ Looks up any [Queryable](https://dataclient.io/rest/api/schema#queryable) [Schem This is used in [useQuery](https://dataclient.io/vue/api/useQuery) and can be used in [Managers](./Manager.vue.md) to safely access the store. -```tsx title="useQuery.ts" -import { - useController, - useCacheState, - type Queryable, - type SchemaArgs, - type DenormalizeNullable, -} from '@data-client/core'; - -/** Oversimplified useQuery */ -function useQuery( - schema: S, - ...args: SchemaArgs -): DenormalizeNullable | undefined { - const state = useCacheState(); - const controller = useController(); - - return controller.get(schema, ...args, state); -} +In components, [useQuery()](https://dataclient.io/vue/api/useQuery) keeps the result reactive. In event handlers, pass +[getState()](#getState) to read the latest store: + +```ts +const ctrl = useController(); + +const toggle = (id: string) => { + const todo = ctrl.get(Todo, { id }, ctrl.getState()); + if (todo) ctrl.set(Todo, { id }, { id, completed: !todo.completed }); +}; ``` ### getResponse(endpoint, ...args, state) {#getResponse} @@ -562,23 +583,8 @@ A number representing time when it expires. Compare to Date.now(). This is used in [useCache](https://dataclient.io/vue/api/useCache), [useSuspense](https://dataclient.io/vue/api/useSuspense) and can be used in [Managers](./Manager.vue.md) to lookup a response with the state provided. -```tsx title="useCache.ts" -import { - useController, - StateContext, - EndpointInterface, -} from '@data-client/core'; - -/** Oversimplified useCache */ -function useCache( - endpoint: E, - ...args: readonly [...Parameters] -) { - const state = useContext(StateContext); - const controller = useController(); - return controller.getResponse(endpoint, ...args, state).data; -} -``` +In event handlers, pass [getState()](#getState) to read the latest store, as in the +[getState() example](#getState). ```tsx title="MyManager.ts" import type { Manager, Middleware, actionTypes } from '@data-client/core'; @@ -623,31 +629,23 @@ Gets the internal state of Reactive Data Client that has _already been [committe > > This should only be used in event handlers or [Managers](./Manager.vue.md). > -> Using getState() in React's render lifecycle can result in data tearing. +> Using getState() in a `computed()` or template won't update when the store changes. Use +> [useQuery()](https://dataclient.io/vue/api/useQuery) or [useCache()](https://dataclient.io/vue/api/useCache) there instead. -```tsx +```ts const controller = useController(); -const updateHandler = useCallback( - async updatePayload => { - const response = await controller.fetch( - MyResource.update, - { id }, - updatePayload, - ); - // the fetch has completed, but react has not yet re-rendered - // this lets use sequence after the next re-render - // we're working on a better solution to this specific case - setTimeout(() => { - const { data: denormalized } = controller.getResponse( - MyResource.update, - { id }, - updatePayload, - controller.getState(), - ); - redirect(denormalized.getterUrl); - }, 40); - }, - [id], -); +const handleRefresh = () => { + const { expiryStatus } = controller.getResponse( + MyResource.get, + { id: props.id }, + controller.getState(), + ); + // only refetch if the data is no longer fresh + if (expiryStatus !== ExpiryStatus.Valid) + controller.fetch(MyResource.get, { id: props.id }); +}; ``` + +Mutations (`sideEffect: true`) resolve _before_ the store is updated, so read their result from the +value `fetch()` resolves with rather than `getState()`. diff --git a/.agents/skills/data-client-manager/references/LogoutManager.md b/.agents/skills/data-client-manager/references/LogoutManager.md index 06428975d00b..2946d38e0027 100644 --- a/.agents/skills/data-client-manager/references/LogoutManager.md +++ b/.agents/skills/data-client-manager/references/LogoutManager.md @@ -158,6 +158,7 @@ const managers = [ > ```ts > import { unAuth } from '../authentication'; > +> const myDomain = 'http://test.com'; > const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); > > const managers = [ diff --git a/.agents/skills/data-client-manager/references/LogoutManager.vue.md b/.agents/skills/data-client-manager/references/LogoutManager.vue.md new file mode 100644 index 000000000000..eb7a4fac994d --- /dev/null +++ b/.agents/skills/data-client-manager/references/LogoutManager.vue.md @@ -0,0 +1,86 @@ + + +# LogoutManager + +Logs out based on fetch responses. By default this is triggered by [401 (Unauthorized)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) status responses. + +> **Info: implements** +> +> `LogoutManager` implements [Manager](./Manager.vue.md) + +## Usage + +```ts title="main.ts" +import { createApp } from 'vue'; +import { + DataClientPlugin, + LogoutManager, + getDefaultManagers, +} from '@data-client/vue'; +import App from './App.vue'; + +const managers = [new LogoutManager(), ...getDefaultManagers()]; + +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); +``` + +### Custom logout handler + +```ts +import { unAuth } from '../authentication'; + +const managers = [ + new LogoutManager({ + handleLogout(controller) { + // call custom unAuth function we defined + unAuth(); + // still reset the store + controller.resetEntireStore(); + }, + }), + ...getDefaultManagers(), +]; +``` + +> **Tip** +> +> Use [controller.invalidateAll](./Controller.vue.md#invalidateAll) to only clear part of the cache. +> +> ```ts +> import { unAuth } from '../authentication'; +> +> const myDomain = 'http://test.com'; +> const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); +> +> const managers = [ +> new LogoutManager({ +> handleLogout(controller) { +> // call custom unAuth function we defined +> unAuth(); +> // still reset the store +> controller.invalidateAll({ testKey }); +> }, +> }), +> ...getDefaultManagers(), +> ]; +> ``` + +## Members + +### handleLogout(controller) + +By default simply calls [controller.resetEntireStore()](./Controller.vue.md#resetEntireStore) + +This should be sufficient if login state is determined by a user entity existance in the Reactive Data Client store. However, +you can override this method via inheritance if more should be done. + +### shouldLogout(error) + +```ts +protected shouldLogout(error: UnknownError) { + // 401 indicates reauthorization is needed + return error.status === 401; +} +``` diff --git a/.agents/skills/data-client-manager/references/Manager.md b/.agents/skills/data-client-manager/references/Manager.md index 96746db6c055..03eb711fd0e9 100644 --- a/.agents/skills/data-client-manager/references/Manager.md +++ b/.agents/skills/data-client-manager/references/Manager.md @@ -30,7 +30,9 @@ interface Manager { ### middleware `middleware` is very similar to a [redux middleware](https://redux.js.org/advanced/middleware). -The only differences is that the `next()` function returns a `Promise`. This promise resolves when the reducer update is +The only differences is that the `next()` function returns a `Promise`. + +This promise resolves when the reducer update is [committed](https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/#general-algorithm) when using \. This is necessary since the commit phase is asynchronously scheduled. This enables building managers that perform work after the DOM is updated and also with the newly computed state. @@ -213,8 +215,8 @@ export default class TimeManager implements Manager { `actionTypes` includes all constants to distinguish between different [actions](./Actions.md). ```ts -import type { Manager, Middleware } from '@data-client/react'; -import { actionTypes } from '@data-client/react'; +import type { Manager, Middleware } from '@data-client/core'; +import { actionTypes } from '@data-client/core'; export default class LoggingManager implements Manager { middleware: Middleware = controller => next => async action => { @@ -224,7 +226,7 @@ export default class LoggingManager implements Manager { console.info( `${action.endpoint.name} ${JSON.stringify(action.response)}`, ); - // wait for state update to be committed to React + // wait for state update to be committed await next(action); // get the data from the store, which may be merged with existing state const { data } = controller.getResponse( @@ -259,8 +261,8 @@ export default function isEntity(schema: Schema): schema is EntityInterface { ``` ```ts title="SubsManager" -import type { Manager, Middleware, EntityInterface } from '@data-client/react'; -import { actionTypes } from '@data-client/react'; +import type { Manager, Middleware, EntityInterface } from '@data-client/core'; +import { actionTypes } from '@data-client/core'; import isEntity from './isEntity'; export default class CustomSubsManager implements Manager { diff --git a/.agents/skills/data-client-manager/references/Manager.vue.md b/.agents/skills/data-client-manager/references/Manager.vue.md index 88ec2bad20b3..323289a34850 100644 --- a/.agents/skills/data-client-manager/references/Manager.vue.md +++ b/.agents/skills/data-client-manager/references/Manager.vue.md @@ -2,7 +2,7 @@ # Manager -`Managers` are singletons that handle global side-effects. Kind of like [useEffect()](https://react.dev/reference/react/useEffect) for the central data +`Managers` are singletons that handle global side-effects. Kind of like [watchEffect()](https://vuejs.org/api/reactivity-core.html#watcheffect) for the central data store. The default managers orchestrate the complex asynchronous behavior that Data Client @@ -30,10 +30,11 @@ interface Manager { ### middleware `middleware` is very similar to a [redux middleware](https://redux.js.org/advanced/middleware). -The only differences is that the `next()` function returns a `Promise`. This promise resolves when the reducer update is -[committed](https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/#general-algorithm) -when using \. This is necessary since the commit phase is asynchronously scheduled. This enables building -managers that perform work after the DOM is updated and also with the newly computed state. +The only differences is that the `next()` function returns a `Promise`. + +This promise resolves when the reducer update is committed to the +[DataClientPlugin](https://dataclient.io/vue/getting-started/installation) store. This enables building managers that perform work with the +newly computed state. Since redux is fully synchronous, an adapter must be placed in front of Reactive Data Client style middleware to ensure they can consume a promise. Conversely, redux middleware must be changed to pass through promises. @@ -55,120 +56,21 @@ Provides any cleanup of dangling resources after manager is no longer in use. Use the `managers` option of [DataClientPlugin](https://dataclient.io/vue/getting-started/installation). The plugin is installed once per app, so managers are created once. -**Web** - -```tsx title="/index.tsx" -import { DataProvider, getDefaultManagers } from '@data-client/react'; -import { createRoot } from 'react-dom/client'; - -const managers = [...getDefaultManagers(), new MyManager()]; - -createRoot(document.body).render( - - - , -); -``` - -**React Native** - -```tsx title="/index.tsx" -import { DataProvider, getDefaultManagers } from '@data-client/react'; -import { AppRegistry } from 'react-native'; +```ts title="main.ts" +import { createApp } from 'vue'; +import { DataClientPlugin, getDefaultManagers } from '@data-client/vue'; +import App from './App.vue'; const managers = [...getDefaultManagers(), new MyManager()]; -const Root = () => ( - - - -); -AppRegistry.registerComponent('MyApp', () => Root); -``` - -**NextJS** - -```tsx title="app/Provider.tsx" -'use client'; -import { getDefaultManagers } from '@data-client/react'; -import { DataProvider } from '@data-client/react/nextjs'; - -const managers = [...getDefaultManagers(), new MyManager()]; - -export default function Provider({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} -``` - -```tsx title="app/_layout.tsx" -import Provider from './Provider'; - -export default function RootLayout({ children }) { - return ( - - - {children} - - - ); -} -``` - -**Expo** - -```tsx title="app/Provider.tsx" -import { getDefaultManagers, DataProvider } from '@data-client/react'; -import { - DarkTheme, - DefaultTheme, - ThemeProvider, -} from '@react-navigation/native'; -import { useColorScheme } from '@/hooks/useColorScheme'; - -const managers = [...getDefaultManagers(), new MyManager()]; - -export default function Provider({ - children, -}: { - children: React.ReactNode; -}) { - const colorScheme = useColorScheme(); - - return ( - - {children} - - ); -} -``` - -```tsx title="app/_layout.tsx" -import { Stack } from 'expo-router'; -import 'react-native-reanimated'; - -import Provider from './Provider'; - -export default function RootLayout() { - return ( - - - - - - - ); -} +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); ``` ## Control flow -Managers integrate with the DataProvider store with their lifecycles and middleware. They orchestrate complex control +Managers integrate with the DataClientPlugin store with their lifecycles and middleware. They orchestrate complex control flows by interfacing via intercepting and dispatching [actions](./Actions.vue.md), as well as reading the internal state. The job of `middleware` is to dispatch actions, respond to [actions](./Actions.vue.md), or both. @@ -212,8 +114,8 @@ export default class TimeManager implements Manager { `actionTypes` includes all constants to distinguish between different [actions](./Actions.vue.md). ```ts -import type { Manager, Middleware } from '@data-client/react'; -import { actionTypes } from '@data-client/react'; +import type { Manager, Middleware } from '@data-client/core'; +import { actionTypes } from '@data-client/core'; export default class LoggingManager implements Manager { middleware: Middleware = controller => next => async action => { @@ -223,7 +125,7 @@ export default class LoggingManager implements Manager { console.info( `${action.endpoint.name} ${JSON.stringify(action.response)}`, ); - // wait for state update to be committed to React + // wait for state update to be committed await next(action); // get the data from the store, which may be merged with existing state const { data } = controller.getResponse( @@ -258,8 +160,8 @@ export default function isEntity(schema: Schema): schema is EntityInterface { ``` ```ts title="SubsManager" -import type { Manager, Middleware, EntityInterface } from '@data-client/react'; -import { actionTypes } from '@data-client/react'; +import type { Manager, Middleware, EntityInterface } from '@data-client/core'; +import { actionTypes } from '@data-client/core'; import isEntity from './isEntity'; export default class CustomSubsManager implements Manager { @@ -312,6 +214,6 @@ Minimal examples for common Manager use cases: - [Cross-tab synchronization](./managers.vue.md#cross-tab-sync) - [Offline persistence](./managers.vue.md#persistence) - [Data streams (websockets/SSE)](./managers.vue.md#data-stream) -- [Authentication: logout on 401](./LogoutManager.md) +- [Authentication: logout on 401](./LogoutManager.vue.md) - [Periodic updates (interval/ticker)](#dispatching-actions) - [Custom transport subscriptions](#reading-and-consuming-actions) diff --git a/.agents/skills/data-client-manager/references/managers.vue.md b/.agents/skills/data-client-manager/references/managers.vue.md index 13a4ad7d80c2..31da8beecf23 100644 --- a/.agents/skills/data-client-manager/references/managers.vue.md +++ b/.agents/skills/data-client-manager/references/managers.vue.md @@ -25,7 +25,7 @@ They can also be customized to change core behaviors. | [SubscriptionManager](https://dataclient.io/vue/api/SubscriptionManager) | Handles polling [subscriptions](https://dataclient.io/vue/getting-started/data-dependency#subscriptions) | | [DevToolsManager](https://dataclient.io/vue/api/DevToolsManager) | Enables [debugging](https://dataclient.io/vue/getting-started/debugging) | | Extra managers | | -| [LogoutManager](./LogoutManager.md) | Handles HTTP `401` (or other logout conditions) | +| [LogoutManager](./LogoutManager.vue.md) | Handles HTTP `401` (or other logout conditions) | ## Examples diff --git a/.agents/skills/data-client-react-testing/references/mockInitialState.md b/.agents/skills/data-client-react-testing/references/mockInitialState.md index f67b05b3eec0..3e0da2dc62a5 100644 --- a/.agents/skills/data-client-react-testing/references/mockInitialState.md +++ b/.agents/skills/data-client-react-testing/references/mockInitialState.md @@ -35,9 +35,9 @@ import MyComponentToTest from 'components/MyComponentToTest'; const results = [ { - request: ArticleResource.getList, - params: { maxResults: 10 }, - result: [ + endpoint: ArticleResource.getList, + args: [{ maxResults: 10 }], + response: [ { id: 5, content: 'have a merry christmas', diff --git a/.agents/skills/data-client-react/references/Controller.md b/.agents/skills/data-client-react/references/Controller.md index eaf1bc603301..6da08e10c2c1 100644 --- a/.agents/skills/data-client-react/references/Controller.md +++ b/.agents/skills/data-client-react/references/Controller.md @@ -91,7 +91,7 @@ function PostListItem({ post }: { post: PostResource }) { await ctrl.fetch(PostResource.delete, { id: post.id }); history.push('/'); }, - [ctrl, id], + [ctrl, post.id], ); return ( @@ -109,7 +109,7 @@ function PostListItem({ post }: { post: PostResource }) { > When using schemas, the denormalized value is returned > > ```ts -> import { useController } from '@data-client/react'; +> const controller = useController(); > > const post = await controller.fetch( > PostResource.getList.push, @@ -175,7 +175,7 @@ when there are many parameterizations in cache. import { type Controller, useController } from '@data-client/react'; const createTradeHandler = (ctrl: Controller) => async trade => { - await ctrl.fetch(TradeResource.getList.push({ user: user.id }, trade)); + await ctrl.fetch(TradeResource.getList.push, { user: user.id }, trade); ctrl.expireAll(AccountResource.get); ctrl.expireAll(AccountResource.getList); }; @@ -200,7 +200,7 @@ function CreateTrade({ id }: { id: string }) { ### invalidate(endpoint, ...args) {#invalidate} -Forces refetching and suspense on [useSuspense](./useSuspense.md) with the same Endpoint +Forces refetching and suspenseon [useSuspense](./useSuspense.md) with the same Endpoint and parameters. ```tsx @@ -210,8 +210,10 @@ function ArticleName({ id }: { id: string }) { return (
-

{article.title}

- +

{article.title}

+
); } @@ -229,7 +231,7 @@ function ArticleName({ id }: { id: string }) { > > ```ts > // deletes MyResource(5) -> // this will resuspend MyResource.get({id: '5'}) +> // this will refetch MyResource.get({id: '5'}) > // and remove it from MyResource.getList > controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' }); > ``` @@ -245,8 +247,10 @@ function ArticleName({ id }: { id: string }) { return (
-

{article.title}

- +

{article.title}

+
); } @@ -258,7 +262,7 @@ function ArticleName({ id }: { id: string }) { Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache. -```tsx +```ts const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); @@ -271,11 +275,16 @@ function useLogout() { It's usually a good idea to also clear cache on 401 (unauthorized) with [LogoutManager](https://dataclient.io/docs/api/LogoutManager) as well. -```ts -import { DataProvider, LogoutManager, getDefaultManagers } from '@data-client/react'; +```tsx +import { + DataProvider, + LogoutManager, + getDefaultManagers, +} from '@data-client/react'; import { createRoot } from 'react-dom/client'; import { unAuth } from '../authentication'; +const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); const managers = [ @@ -304,7 +313,7 @@ Resets/clears the entire Reactive Data Client cache. All inflight requests will This is typically used when logging out or changing authenticated users. ```tsx -const USER_NUMBER_ONE: string = "1111"; +const USER_NUMBER_ONE: string = '1111'; function UserName() { const user = useSuspense(CurrentUserResource.get); @@ -317,7 +326,7 @@ function UserName() { }, [ctrl]); return (
-

{user.name}

+

{user.name}

); @@ -494,18 +503,19 @@ This is used in [useQuery](./useQuery.md) and can be used in ```tsx title="useQuery.ts" import { useController, - useCacheState, + StateContext, type Queryable, type SchemaArgs, type DenormalizeNullable, -} from '@data-client/core'; +} from '@data-client/react'; +import { useContext } from 'react'; /** Oversimplified useQuery */ function useQuery( schema: S, ...args: SchemaArgs ): DenormalizeNullable | undefined { - const state = useCacheState(); + const state = useContext(StateContext); const controller = useController(); return controller.get(schema, ...args, state); @@ -566,11 +576,12 @@ This is used in [useCache](./useCache.md), [useSuspense](./useSuspense.md) and c import { useController, StateContext, - EndpointInterface, -} from '@data-client/core'; + type EndpointInterface, +} from '@data-client/react'; +import { useContext } from 'react'; /** Oversimplified useCache */ -function useCache( +function useCache( endpoint: E, ...args: readonly [...Parameters] ) { diff --git a/.agents/skills/data-client-rest/references/expiry-policy.md b/.agents/skills/data-client-rest/references/expiry-policy.md index 0ed3ee46bf31..6e39378e1c1e 100644 --- a/.agents/skills/data-client-rest/references/expiry-policy.md +++ b/.agents/skills/data-client-rest/references/expiry-policy.md @@ -25,8 +25,7 @@ In these cases it will fetch if the data is considered stale. ### Invalid -Data should not be shown. Any components needing this data will trigger fetch and suspense. If -no components care about this data no action will be taken. +Data should not be shown. Any components needing this data will trigger fetch and suspense. If no components care about this data no action will be taken. ## Expiry Time diff --git a/.agents/skills/data-client-rest/references/expiry-policy.vue.md b/.agents/skills/data-client-rest/references/expiry-policy.vue.md index 8d608580b39a..b0a09af5ecea 100644 --- a/.agents/skills/data-client-rest/references/expiry-policy.vue.md +++ b/.agents/skills/data-client-rest/references/expiry-policy.vue.md @@ -21,8 +21,9 @@ In these cases it will fetch if the data is considered stale. ### Invalid -Data should not be shown. Any components needing this data will trigger fetch and suspense. If -no components care about this data no action will be taken. +Data should not be shown. Any components needing this data will trigger fetch. +Components mounting afterward wait for that fetch, while mounted components keep showing their current +data until it resolves. If no components care about this data no action will be taken. ## Expiry Time diff --git a/.agents/skills/data-client-vue/references/Controller.md b/.agents/skills/data-client-vue/references/Controller.md index 390f59f258ec..7076771ea5b1 100644 --- a/.agents/skills/data-client-vue/references/Controller.md +++ b/.agents/skills/data-client-vue/references/Controller.md @@ -46,61 +46,72 @@ the response or error upon completion. **Create** -```tsx -function CreatePost() { +```html title="CreatePost.vue" + + + ``` **Update** -```tsx -function UpdatePost({ id }: { id: string }) { +```html title="UpdatePost.vue" + + + ``` **Delete** -```tsx -function PostListItem({ post }: { post: PostResource }) { - const ctrl = useController(); +```html title="PostListItem.vue" + + + ``` > **Tip** @@ -109,7 +120,7 @@ function PostListItem({ post }: { post: PostResource }) { > When using schemas, the denormalized value is returned > > ```ts -> import { useController } from '@data-client/react'; +> const controller = useController(); > > const post = await controller.fetch( > PostResource.getList.push, @@ -162,8 +173,6 @@ An [example](https://stackblitz.com/github/reactive/data-client/tree/master/exam }, ``` -Example app: [github-app](https://github.com/reactive/data-client/tree/master/examples/github-app) ([`src/routing/routes.tsx`](https://github.com/reactive/data-client/blob/master/examples/github-app/src/routing/routes.tsx)) - ### expireAll({ testKey }) {#expireAll} Sets all responses' [expiry status](https://dataclient.io/vue/concepts/expiry-policy) matching `testKey` to [Stale](https://dataclient.io/vue/concepts/expiry-policy#stale). @@ -171,26 +180,28 @@ Sets all responses' [expiry status](https://dataclient.io/vue/concepts/expiry-po This is sometimes useful to trigger refresh of only data presently shown when there are many parameterizations in cache. -```tsx -import { type Controller, useController } from '@data-client/react'; +```html title="CreateTrade.vue" + - return ( -
- - - - - ); -} + ``` > **Tip** @@ -200,27 +211,32 @@ function CreateTrade({ id }: { id: string }) { ### invalidate(endpoint, ...args) {#invalidate} -Forces refetching and suspense on [useSuspense](./useSuspense.md) with the same Endpoint -and parameters. +Forces refetching on [useSuspense](./useSuspense.md) with the same Endpoint +and parameters. Vue can't suspend a component again after setup, so `useSuspense()` keeps returning the +current data until the refetch resolves. -```tsx -function ArticleName({ id }: { id: string }) { - const article = useSuspense(ArticleResource.get, { id }); - const ctrl = useController(); +```html title="ArticleName.vue" + + + ``` -> **Tip** -> -> To refresh while continuing to display stale data - [Controller.fetch](#fetch). - > **Tip: Invalidate many endpoints at once** > > Use [schema.Invalidate](https://dataclient.io/rest/api/Invalidate) to invalidate every endpoint that contains a given entity. @@ -229,7 +245,7 @@ function ArticleName({ id }: { id: string }) { > > ```ts > // deletes MyResource(5) -> // this will resuspend MyResource.get({id: '5'}) +> // this will refetch MyResource.get({id: '5'}) > // and remove it from MyResource.getList > controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' }); > ``` @@ -238,27 +254,31 @@ function ArticleName({ id }: { id: string }) { [Invalidates](https://dataclient.io/vue/concepts/expiry-policy#invalid) all [endpoint keys](https://dataclient.io/rest/api/RestEndpoint#key) matching `testKey`. -```tsx -function ArticleName({ id }: { id: string }) { - const article = useSuspense(ArticleResource.get, { id }); - const ctrl = useController(); +```html title="ArticleName.vue" + + + ``` -> **Tip** -> -> To refresh while continuing to display stale data - [Controller.expireAll](#expireAll) instead. - Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache. -```tsx +```ts const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); @@ -271,11 +291,17 @@ function useLogout() { It's usually a good idea to also clear cache on 401 (unauthorized) with [LogoutManager](https://dataclient.io/vue/api/LogoutManager) as well. -```ts -import { DataProvider, LogoutManager, getDefaultManagers } from '@data-client/react'; -import { createRoot } from 'react-dom/client'; +```ts title="main.ts" +import { createApp } from 'vue'; +import { + DataClientPlugin, + LogoutManager, + getDefaultManagers, +} from '@data-client/vue'; import { unAuth } from '../authentication'; +import App from './App.vue'; +const myDomain = 'http://test.com'; const testKey = (key: string) => key.startsWith(`GET ${myDomain}`); const managers = [ @@ -290,11 +316,9 @@ const managers = [ ...getDefaultManagers(), ]; -createRoot(document.body).render( - - - , -); +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); ``` ### resetEntireStore() {#resetEntireStore} @@ -303,25 +327,29 @@ Resets/clears the entire Reactive Data Client cache. All inflight requests will This is typically used when logging out or changing authenticated users. -```tsx -const USER_NUMBER_ONE: string = "1111"; +```html title="UserName.vue" + + + ``` ### set(queryable, ...args, value) {#set} @@ -421,11 +449,12 @@ Any components suspending for the given [Endpoint](https://dataclient.io/rest/ap If data already exists for the given [Endpoint](https://dataclient.io/rest/api/Endpoint) and args, it will be updated. -```tsx +```ts const ctrl = useController(); +let websocket: WebSocket; -useEffect(() => { - const websocket = new Websocket(url); +onMounted(() => { + websocket = new WebSocket(url); websocket.onmessage = event => ctrl.setResponse( @@ -433,12 +462,12 @@ useEffect(() => { ...event.args, event.data, ); - - return () => websocket.close(); }); + +onUnmounted(() => websocket.close()); ``` -This shows a proof of concept in React; however a [Manager websockets implementation](https://dataclient.io/vue/concepts/managers#data-stream) +This shows a proof of concept in Vue; however a [Manager websockets implementation](https://dataclient.io/vue/concepts/managers#data-stream) would be much more robust. ### setError(endpoint, ...args, error) {#setError} @@ -461,16 +490,17 @@ Marks a new subscription to a given [Endpoint](https://dataclient.io/rest/api/En [useSubscription](./useSubscription.md) and [useLive](./useLive.md) call this on mount. -This might be useful for custom hooks to sub/unsub based on other factors. +This might be useful for custom composables to sub/unsub based on other factors. -```tsx +```ts const controller = useController(); -const key = endpoint.key(...args); -useEffect(() => { - controller.subscribe(endpoint, ...args); - return () => controller.unsubscribe(endpoint, ...args); -}, [controller, key]); +// args can be a ref, computed or getter; this re-runs when it changes +watchEffect(onCleanup => { + const currentArgs = toValue(args); + controller.subscribe(endpoint, ...currentArgs); + onCleanup(() => controller.unsubscribe(endpoint, ...currentArgs)); +}); ``` ### unsubscribe(endpoint, ...args) {#unsubscribe} @@ -491,25 +521,16 @@ Looks up any [Queryable](https://dataclient.io/rest/api/schema#queryable) [Schem This is used in [useQuery](./useQuery.md) and can be used in [Managers](https://dataclient.io/vue/api/Manager) to safely access the store. -```tsx title="useQuery.ts" -import { - useController, - useCacheState, - type Queryable, - type SchemaArgs, - type DenormalizeNullable, -} from '@data-client/core'; - -/** Oversimplified useQuery */ -function useQuery( - schema: S, - ...args: SchemaArgs -): DenormalizeNullable | undefined { - const state = useCacheState(); - const controller = useController(); - - return controller.get(schema, ...args, state); -} +In components, [useQuery()](./useQuery.md) keeps the result reactive. In event handlers, pass +[getState()](#getState) to read the latest store: + +```ts +const ctrl = useController(); + +const toggle = (id: string) => { + const todo = ctrl.get(Todo, { id }, ctrl.getState()); + if (todo) ctrl.set(Todo, { id }, { id, completed: !todo.completed }); +}; ``` ### getResponse(endpoint, ...args, state) {#getResponse} @@ -562,23 +583,8 @@ A number representing time when it expires. Compare to Date.now(). This is used in [useCache](./useCache.md), [useSuspense](./useSuspense.md) and can be used in [Managers](https://dataclient.io/vue/api/Manager) to lookup a response with the state provided. -```tsx title="useCache.ts" -import { - useController, - StateContext, - EndpointInterface, -} from '@data-client/core'; - -/** Oversimplified useCache */ -function useCache( - endpoint: E, - ...args: readonly [...Parameters] -) { - const state = useContext(StateContext); - const controller = useController(); - return controller.getResponse(endpoint, ...args, state).data; -} -``` +In event handlers, pass [getState()](#getState) to read the latest store, as in the +[getState() example](#getState). ```tsx title="MyManager.ts" import type { Manager, Middleware, actionTypes } from '@data-client/core'; @@ -623,31 +629,23 @@ Gets the internal state of Reactive Data Client that has _already been [committe > > This should only be used in event handlers or [Managers](https://dataclient.io/vue/api/Manager). > -> Using getState() in React's render lifecycle can result in data tearing. +> Using getState() in a `computed()` or template won't update when the store changes. Use +> [useQuery()](./useQuery.md) or [useCache()](./useCache.md) there instead. -```tsx +```ts const controller = useController(); -const updateHandler = useCallback( - async updatePayload => { - const response = await controller.fetch( - MyResource.update, - { id }, - updatePayload, - ); - // the fetch has completed, but react has not yet re-rendered - // this lets use sequence after the next re-render - // we're working on a better solution to this specific case - setTimeout(() => { - const { data: denormalized } = controller.getResponse( - MyResource.update, - { id }, - updatePayload, - controller.getState(), - ); - redirect(denormalized.getterUrl); - }, 40); - }, - [id], -); +const handleRefresh = () => { + const { expiryStatus } = controller.getResponse( + MyResource.get, + { id: props.id }, + controller.getState(), + ); + // only refetch if the data is no longer fresh + if (expiryStatus !== ExpiryStatus.Valid) + controller.fetch(MyResource.get, { id: props.id }); +}; ``` + +Mutations (`sideEffect: true`) resolve _before_ the store is updated, so read their result from the +value `fetch()` resolves with rather than `getState()`. diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index 6437db27203d..da6c613d07ec 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -330,7 +330,11 @@ function CreateTrade({ id }: { id: string }) { const ctrl = useController(); const handleTrade = async (trade: Trade) => { - await ctrl.fetch(TradeResource.getList.push, { user: props.userId }, trade); + await ctrl.fetch( + TradeResource.getList.push, + { user: props.userId }, + trade, + ); // highlight-start ctrl.expireAll(AccountResource.get); ctrl.expireAll(AccountResource.getList); @@ -368,7 +372,9 @@ function ArticleName({ id }: { id: string }) { return (

{article.title}

- +
); } @@ -380,16 +386,14 @@ function ArticleName({ id }: { id: string }) { ```html title="ArticleName.vue"