@servicetitan/docs-anvil-uikit-contrib 38.7.0 → 39.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/intl/date-time.mdx +2 -0
- package/docs/tanstack-query-mobx/01-queries.mdx +187 -0
- package/docs/tanstack-query-mobx/02-mutations.mdx +141 -0
- package/docs/tanstack-query-mobx/03-extending-stores.mdx +140 -0
- package/docs/tanstack-query-mobx/04-shared-clients.mdx +63 -0
- package/docs/tanstack-query-mobx/05-query-client-store.mdx +156 -0
- package/docs/tanstack-query-mobx/06-testing.mdx +74 -0
- package/docs/{tanstack-query-mobx.mdx → tanstack-query-mobx/07-examples.mdx} +10 -14
- package/docs/tanstack-query-mobx/index.mdx +119 -0
- package/package.json +2 -2
package/docs/intl/date-time.mdx
CHANGED
|
@@ -20,6 +20,8 @@ The following standard ServiceTitan formats are available (examples are in `en-U
|
|
|
20
20
|
| `long` | Long date and short time | `January 9, 2025 at 4:00 PM` |
|
|
21
21
|
| `longWithTimeZone` | Long date and short time, with timezone | `January 9, 2025 at 4:00 PM EST` |
|
|
22
22
|
|
|
23
|
+
Any options passed to `FormattedDateTime` or `formatDateTime` will be merged on top of `{ format: 'short' }` (which expands to `{ day: '2-digit', month: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric' }` [Intl.DateTimeFormatOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options)). If you want to use your custom options only, you can pass `format: undefined` to disable the default.
|
|
24
|
+
|
|
23
25
|
## Usage
|
|
24
26
|
|
|
25
27
|
### FormattedDateTime
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Queries
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Queries
|
|
6
|
+
|
|
7
|
+
Queries are **data-driven** — they run automatically based on their options, not by explicit calls. When a store initializes, its queries subscribe to the TanStack client and begin fetching. Changes to `queryKey` values trigger refetches via MobX reactivity.
|
|
8
|
+
|
|
9
|
+
## Creating Queries
|
|
10
|
+
|
|
11
|
+
### Composable (recommended)
|
|
12
|
+
|
|
13
|
+
Use the `query()` factory as a class field with a function `() => ({...})`. MobX tracks observable dependencies accessed inside the function and triggers refetch when they change.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
@queryStore
|
|
17
|
+
class JobsStore extends Store {
|
|
18
|
+
@inject(JobsApi) private api?: JobsApi;
|
|
19
|
+
@observable selectedJobId = 0;
|
|
20
|
+
|
|
21
|
+
jobs = query<Job[]>(() => ({
|
|
22
|
+
queryKey: ['scheduling', 'jobs', this.selectedJobId],
|
|
23
|
+
queryFn: async () => (await this.api?.getJobs(this.selectedJobId))?.data ?? [],
|
|
24
|
+
enabled: this.selectedJobId > 0,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
techs = query<Tech[]>(() => ({
|
|
28
|
+
queryKey: ['scheduling', 'techs'],
|
|
29
|
+
queryFn: async () => (await this.api?.getTechs())?.data ?? [],
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Multiple queries are just additional class fields — no special API needed.
|
|
35
|
+
|
|
36
|
+
> **Note:** `query()` also accepts a plain object `query({...})` instead of a function, but this is evaluated once at class field initialization and won't react to observable changes. Use the function form unless you have a reason not to.
|
|
37
|
+
|
|
38
|
+
### Inheritance
|
|
39
|
+
|
|
40
|
+
Use the `queryOptions` getter for the primary query, and `addQuery()` for additional queries.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
@injectable()
|
|
44
|
+
class JobsStore extends QueryApiStore<Job[]> {
|
|
45
|
+
@inject(JobsApi) private api?: JobsApi;
|
|
46
|
+
|
|
47
|
+
get queryOptions(): QueryApiOptions<Job[]> {
|
|
48
|
+
return {
|
|
49
|
+
queryKey: ['scheduling', 'jobs'],
|
|
50
|
+
queryFn: async () => (await this.api?.getJobs())?.data ?? [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
techs = this.addQuery<Tech[]>(() => ({
|
|
55
|
+
queryKey: ['scheduling', 'techs'],
|
|
56
|
+
queryFn: async () => (await this.api?.getTechs())?.data ?? [],
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Query Options
|
|
62
|
+
|
|
63
|
+
### queryKey (required)
|
|
64
|
+
|
|
65
|
+
Array of serializable values that uniquely identifies the query.
|
|
66
|
+
|
|
67
|
+
- Use at least two entries — the first as a **namespace** string (e.g., `'scheduling'`, `'myapp'`) to scope and organize queries
|
|
68
|
+
- Changes to any value in the key trigger a refetch (data-driven query)
|
|
69
|
+
- Multiple stores with the same key share one cache and one network request (deduplication)
|
|
70
|
+
- If data is already cached for a key, new subscribers get the cached data immediately (respecting `staleTime`)
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
queryKey: ['scheduling', 'jobs'], // namespace + resource
|
|
74
|
+
queryKey: ['scheduling', 'jobs', jobId], // refetches when jobId changes
|
|
75
|
+
queryKey: ['scheduling', 'jobs', [...ids].sort()], // sort arrays so [1,2] and [2,1] hit the same cache
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### queryFn (required)
|
|
79
|
+
|
|
80
|
+
Async function that fetches and returns data. Can use any mechanism — Axios, fetch, generated API functions, or just return a value.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
queryFn: async () => (await this.api?.getJobs())?.data ?? [],
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If the function depends on data like an ID, include that data in `queryKey` so the function re-runs when it changes.
|
|
87
|
+
|
|
88
|
+
### enabled
|
|
89
|
+
|
|
90
|
+
Boolean, defaults to `true`. The query only runs when `enabled` is `true`. Toggling from `false` to `true` triggers a fetch.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
enabled: this.selectedJobId > 0, // wait until an ID is selected
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### staleTime
|
|
97
|
+
|
|
98
|
+
Milliseconds before cached data is considered stale. Default: **10 minutes** (set by `QueryClientStore`).
|
|
99
|
+
|
|
100
|
+
- **`Infinity`** — data never goes stale. Only one fetch per key, no matter how many stores subscribe. Refetch only via manual `refetch()` or `invalidate()`.
|
|
101
|
+
- **`0`** — data is always stale. Every new subscriber or key change triggers a refetch.
|
|
102
|
+
- **`10 * 60 * 1000` (default)** — data is fresh for 10 minutes. New subscribers within that window get cached data without refetching.
|
|
103
|
+
|
|
104
|
+
Stale queries re-fetch on **fetch events**: a new store subscribing with the same key, or a `queryKey` value changing. Re-fetches are deduplicated — only one network request per key at a time.
|
|
105
|
+
|
|
106
|
+
### gcTime (garbage collection)
|
|
107
|
+
|
|
108
|
+
Milliseconds before an **inactive** query is garbage collected. Default: **5 minutes**. A query becomes inactive when nothing is subscribed to it — either because the store unmounted, or because a reactive `queryKey` change shifted the observer to a different key.
|
|
109
|
+
|
|
110
|
+
### retry
|
|
111
|
+
|
|
112
|
+
Number of retry attempts for failed queries. Default: **3** with exponential backoff.
|
|
113
|
+
|
|
114
|
+
### refetchInterval
|
|
115
|
+
|
|
116
|
+
Number or function. When set, refetches at the specified interval. The interval resets after each refresh.
|
|
117
|
+
|
|
118
|
+
See all options at [TanStack Query useQuery reference](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery).
|
|
119
|
+
|
|
120
|
+
## Integration-Specific Options
|
|
121
|
+
|
|
122
|
+
These are additions to TanStack's standard options:
|
|
123
|
+
|
|
124
|
+
| Option | Default | Description |
|
|
125
|
+
|--------|---------|-------------|
|
|
126
|
+
| `disposeQuery` | `undefined` | Remove the query from the client cache on dispose. `true` = exact key, number = first N key segments |
|
|
127
|
+
| `autoInitialize` | `true` | Set `initialized` to `true` after first successful fetch. Set to `false` to control it manually |
|
|
128
|
+
| `onSuccess` | `undefined` | Callback with query data after success. Also fires on `updateQueryData()` |
|
|
129
|
+
| `onError` | `undefined` | Callback with error object after failure (after retries) |
|
|
130
|
+
| `globalClient` | `undefined` | Use a shared page or app-level query client. See [Shared Clients](./04-shared-clients) |
|
|
131
|
+
|
|
132
|
+
## Query State
|
|
133
|
+
|
|
134
|
+
Each query exposes observable state:
|
|
135
|
+
|
|
136
|
+
| Flag | When `true` |
|
|
137
|
+
|------|-------------|
|
|
138
|
+
| `initialized` | After the first query attempt settles (package-specific, not in TanStack) |
|
|
139
|
+
| `isPending` | No settled result yet — includes disabled queries that have never run |
|
|
140
|
+
| `isLoading` | First fetch in flight (`isFetching && isPending`) — use for initial loading UI |
|
|
141
|
+
| `isFetching` | Any fetch in flight, including background refetches |
|
|
142
|
+
| `isRefetching` | Background refetch after data already exists |
|
|
143
|
+
| `isSuccess` | Query succeeded |
|
|
144
|
+
| `isError` | Query failed (after retries) |
|
|
145
|
+
|
|
146
|
+
**Which to use:**
|
|
147
|
+
- **Initial loading spinner**: `!initialized` — false until the first query attempt settles
|
|
148
|
+
- **Background refresh indicator**: `isRefetching`
|
|
149
|
+
- **Error handling**: `isError` + `error`
|
|
150
|
+
|
|
151
|
+
## Query Methods
|
|
152
|
+
|
|
153
|
+
| Method | Description |
|
|
154
|
+
|--------|-------------|
|
|
155
|
+
| `invalidate()` | Mark query as stale and refetch. Supports `dedupe` option to skip if already invalidated |
|
|
156
|
+
| `refetch()` | Manually re-run the query |
|
|
157
|
+
| `updateQueryData(data)` | Update cache without fetching. Triggers `onSuccess` |
|
|
158
|
+
| `cancel()` | Cancel an in-flight request |
|
|
159
|
+
| `getQueryState()` | Get the full TanStack query state object |
|
|
160
|
+
|
|
161
|
+
## Runtime Queries
|
|
162
|
+
|
|
163
|
+
For queries created after initialization (e.g., in response to user actions), use `setupRuntimeQueries()`:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import { setupRuntimeQueries } from '@servicetitan/tanstack-query-mobx/composable';
|
|
167
|
+
|
|
168
|
+
@queryStore
|
|
169
|
+
class DetailsStore extends Store {
|
|
170
|
+
private queries = setupRuntimeQueries();
|
|
171
|
+
|
|
172
|
+
loadDetails(id: number) {
|
|
173
|
+
// Second arg is the key — calling add() again with the same key returns the existing query
|
|
174
|
+
return this.queries.add<Details>(() => ({
|
|
175
|
+
queryKey: ['details', id],
|
|
176
|
+
queryFn: () => this.api?.getDetails(id),
|
|
177
|
+
}), ['details', id]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getDetails(id: number) {
|
|
181
|
+
// Retrieve a previously created query by key
|
|
182
|
+
return this.queries.get<Details>(['details', id]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Queries created via `.add()` are automatically set up with the store's query client and disposed when the store disposes. The key serves two purposes: `.add()` returns the existing query if one was already created with that key, and `.get()` retrieves it later.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Mutations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Mutations
|
|
6
|
+
|
|
7
|
+
Mutations are for updating, creating, or deleting data. Unlike queries, mutations are **called manually** via `runMutation()`. They can automatically invalidate (refetch) related queries after success.
|
|
8
|
+
|
|
9
|
+
## Creating Mutations
|
|
10
|
+
|
|
11
|
+
### Composable (recommended)
|
|
12
|
+
|
|
13
|
+
Use the `mutation()` factory as a class field:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
@queryStore
|
|
17
|
+
class TasksStore extends Store {
|
|
18
|
+
@inject(TasksApi) private api?: TasksApi;
|
|
19
|
+
|
|
20
|
+
snooze = mutation<void, { id: number; note?: string }>(() => ({
|
|
21
|
+
mutationFn: async (arg) => (await this.api?.snooze(arg.id, arg.note))?.data,
|
|
22
|
+
invalidatedQueries: [['myapp', 'tasks'], ['myapp', 'progress-tracker']],
|
|
23
|
+
onSuccess: () => {
|
|
24
|
+
toast.success({ title: 'Task Snoozed', message: 'We will check back soon.' });
|
|
25
|
+
},
|
|
26
|
+
onError: error => {
|
|
27
|
+
toast.danger({ title: 'Task Snooze', message: error.message });
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
handleSnoozed = async (id: number, note?: string) => {
|
|
32
|
+
await this.snooze.runMutation({ id, note });
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Inheritance
|
|
38
|
+
|
|
39
|
+
Use `addMutation()` on the store:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
@injectable()
|
|
43
|
+
class TasksStore extends QueryApiStore<Task[]> {
|
|
44
|
+
@inject(TasksApi) private api?: TasksApi;
|
|
45
|
+
|
|
46
|
+
snooze = this.addMutation<void, { id: number; note?: string }>(() => ({
|
|
47
|
+
mutationFn: async (arg) => (await this.api?.snooze(arg.id, arg.note))?.data,
|
|
48
|
+
invalidatedQueries: [['myapp', 'tasks']],
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Mutation Options
|
|
54
|
+
|
|
55
|
+
### mutationKey
|
|
56
|
+
|
|
57
|
+
Array of serializable data. Optional — not needed for most use cases.
|
|
58
|
+
|
|
59
|
+
### mutationFn (required)
|
|
60
|
+
|
|
61
|
+
Async function called when `runMutation()` is invoked. The argument is whatever you pass to `runMutation()` — an object, a single value, or nothing (`void`).
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
mutationFn: async (arg) => (await this.api?.deleteJob(arg.id))?.data,
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### invalidatedQueries
|
|
68
|
+
|
|
69
|
+
Array of query keys to invalidate (refetch) after a successful mutation. Using the first key in the array as a namespace will invalidate all queries starting with that key.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
invalidatedQueries: [
|
|
73
|
+
['myapp', 'tasks'], // invalidates queries with these first two keys
|
|
74
|
+
['myapp', 'progress-tracker'],
|
|
75
|
+
['myapp'], // invalidates ALL queries under 'myapp'
|
|
76
|
+
],
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### onSuccess / onError
|
|
80
|
+
|
|
81
|
+
Callbacks after the mutation completes. `onSuccess` receives the return value of `mutationFn`. `onError` receives the error object. These can be set both in the mutation options and in `runMutation()` — both will run.
|
|
82
|
+
|
|
83
|
+
### retry
|
|
84
|
+
|
|
85
|
+
Number of retry attempts. Default: **0** (no retries for mutations, unlike queries).
|
|
86
|
+
|
|
87
|
+
See all options at [TanStack Query useMutation reference](https://tanstack.com/query/v5/docs/framework/react/reference/useMutation).
|
|
88
|
+
|
|
89
|
+
## Integration-Specific Options
|
|
90
|
+
|
|
91
|
+
| Option | Description |
|
|
92
|
+
|--------|-------------|
|
|
93
|
+
| `invalidatedQueries` | Query keys to refetch after successful mutation |
|
|
94
|
+
| `globalClient` | Use a shared page or app-level client (see [Shared Clients](./04-shared-clients)) |
|
|
95
|
+
|
|
96
|
+
## Mutation State
|
|
97
|
+
|
|
98
|
+
Each mutation exposes observable state:
|
|
99
|
+
|
|
100
|
+
| Flag | When `true` |
|
|
101
|
+
|------|-------------|
|
|
102
|
+
| `isPending` | Mutation is in flight |
|
|
103
|
+
| `isSuccess` | Mutation completed successfully |
|
|
104
|
+
| `isError` | Mutation failed (after retries) |
|
|
105
|
+
|
|
106
|
+
## Calling Mutations
|
|
107
|
+
|
|
108
|
+
Use `runMutation()` to execute. The argument matches the `mutationFn` parameter type:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// Object argument
|
|
112
|
+
await store.snooze.runMutation({ id: 42, note: 'Check back later' });
|
|
113
|
+
|
|
114
|
+
// With inline callbacks
|
|
115
|
+
await store.deleteJob.runMutation(
|
|
116
|
+
{ id: 123 },
|
|
117
|
+
{
|
|
118
|
+
onSuccess: () => console.log('Deleted'),
|
|
119
|
+
onError: (error) => console.error('Failed', error),
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Runtime Mutations
|
|
125
|
+
|
|
126
|
+
For mutations created after initialization, use `setupRuntimeMutations()`:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { setupRuntimeMutations } from '@servicetitan/tanstack-query-mobx/composable';
|
|
130
|
+
|
|
131
|
+
@queryStore
|
|
132
|
+
class ActionsStore extends Store {
|
|
133
|
+
private mutations = setupRuntimeMutations();
|
|
134
|
+
|
|
135
|
+
setupDelete(entityId: number) {
|
|
136
|
+
return this.mutations.add<void, { id: number }>(() => ({
|
|
137
|
+
mutationFn: (args) => this.api?.delete(entityId, args),
|
|
138
|
+
}), ['delete', entityId]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Extending Stores
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Extending Stores
|
|
6
|
+
|
|
7
|
+
## Composable — Shared Factory Functions
|
|
8
|
+
|
|
9
|
+
Share query definitions across stores with factory functions. Pass a function that returns the observables and services the query needs — MobX tracks them reactively:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// shared/queries/jobs-queries.ts
|
|
13
|
+
export const jobsQuery = (
|
|
14
|
+
deps: () => { api?: JobsApi; teamId: number } & Partial<QueryApiOptions<Job[]>>
|
|
15
|
+
) => query<Job[]>(() => {
|
|
16
|
+
const { api, teamId, ...overrides } = deps();
|
|
17
|
+
return {
|
|
18
|
+
queryKey: ['scheduling', 'jobs', teamId],
|
|
19
|
+
queryFn: async () => (await api?.getJobs(teamId))?.data ?? [],
|
|
20
|
+
enabled: teamId > 0,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Different stores reuse the same definition
|
|
26
|
+
@queryStore
|
|
27
|
+
class SchedulingStore extends Store {
|
|
28
|
+
@inject(JobsApi) private api?: JobsApi;
|
|
29
|
+
@observable teamId = 0;
|
|
30
|
+
|
|
31
|
+
jobs = jobsQuery(() => ({ api: this.api, teamId: this.teamId }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Consumer with reactive overrides — MobX tracks everything inside the function
|
|
35
|
+
@queryStore
|
|
36
|
+
class DashboardStore extends Store {
|
|
37
|
+
@inject(JobsApi) private api?: JobsApi;
|
|
38
|
+
@observable teamId = 1;
|
|
39
|
+
@observable hasPermission = false;
|
|
40
|
+
|
|
41
|
+
jobs = jobsQuery(() => ({
|
|
42
|
+
api: this.api,
|
|
43
|
+
teamId: this.teamId,
|
|
44
|
+
enabled: this.teamId > 0 && this.hasPermission,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Composable — Getter Override Pattern
|
|
50
|
+
|
|
51
|
+
Use a `@computed` getter on the base class, override in subclasses:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
@queryStore
|
|
55
|
+
class JobsStore extends Store {
|
|
56
|
+
@inject(JobsApi) protected api?: JobsApi;
|
|
57
|
+
|
|
58
|
+
constructor() {
|
|
59
|
+
super();
|
|
60
|
+
makeObservable(this);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
jobs = query<Job[]>(() => this.jobsOptions);
|
|
64
|
+
|
|
65
|
+
@computed
|
|
66
|
+
get jobsOptions(): QueryApiOptions<Job[]> {
|
|
67
|
+
return {
|
|
68
|
+
queryKey: ['scheduling', 'jobs'],
|
|
69
|
+
queryFn: async () => (await this.api?.getJobs())?.data ?? [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Override the getter — @override replaces @computed in subclass
|
|
75
|
+
@queryStore
|
|
76
|
+
class FilteredJobsStore extends JobsStore {
|
|
77
|
+
@observable filters = {};
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
get jobsOptions(): QueryApiOptions<Job[]> {
|
|
81
|
+
return {
|
|
82
|
+
...super.jobsOptions,
|
|
83
|
+
queryKey: ['scheduling', 'jobs', this.filters],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Inheritance — Extending QueryApiStore
|
|
90
|
+
|
|
91
|
+
Create a base store with default options, then extend and override:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
@injectable()
|
|
95
|
+
class JobsStore extends QueryApiStore<Job[]> {
|
|
96
|
+
@inject(JobsApi) protected api?: JobsApi;
|
|
97
|
+
|
|
98
|
+
get queryOptions(): QueryApiOptions<Job[]> {
|
|
99
|
+
return {
|
|
100
|
+
queryKey: ['scheduling', 'jobs'],
|
|
101
|
+
queryFn: async () => (await this.api?.getJobs())?.data ?? [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Override — spread super, tweak what you need
|
|
107
|
+
@injectable()
|
|
108
|
+
class FilteredJobsStore extends JobsStore {
|
|
109
|
+
@observable teamId = 0;
|
|
110
|
+
|
|
111
|
+
get queryOptions(): QueryApiOptions<Job[]> {
|
|
112
|
+
return {
|
|
113
|
+
...super.queryOptions,
|
|
114
|
+
queryKey: ['scheduling', 'jobs', this.teamId],
|
|
115
|
+
enabled: this.teamId > 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Refresh on Mount
|
|
122
|
+
|
|
123
|
+
When navigating between components, you may want to ensure that data from **other** stores is fresh when a store mounts.
|
|
124
|
+
|
|
125
|
+
Use the `refreshOnMount()` factory as a class field. The `@queryStore` decorator executes it automatically during `initialize()`:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { refreshOnMount } from '@servicetitan/tanstack-query-mobx/composable';
|
|
129
|
+
|
|
130
|
+
@queryStore
|
|
131
|
+
class JobsStore extends Store {
|
|
132
|
+
jobs = query<Job[]>(() => ({ ... }));
|
|
133
|
+
|
|
134
|
+
// Ensure fresh data from external store caches when this store mounts
|
|
135
|
+
refresh = refreshOnMount(['business-units', 'list'], ['techs', 'list']);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Accepts any number of `QueryKey` arrays and `QueryApi` instances, comma-separated.
|
|
140
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shared Clients
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Shared Query Clients
|
|
6
|
+
|
|
7
|
+
To sync and share cached data across multiple Micro Frontends (MFEs), a shared query client can be used at the **page** or **app** level.
|
|
8
|
+
|
|
9
|
+
## Page-Level Client
|
|
10
|
+
|
|
11
|
+
Reference-counted — disposes when the last consumer unmounts. Two ways to set up:
|
|
12
|
+
|
|
13
|
+
### Top-level provider (preferred)
|
|
14
|
+
|
|
15
|
+
Propagates to all stores underneath:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { getQueryClient } from '@servicetitan/tanstack-query-mobx/composable';
|
|
19
|
+
|
|
20
|
+
export const App: FC = provide({
|
|
21
|
+
singletons: [getQueryClient('page')],
|
|
22
|
+
})(observer(() => <YourApp />));
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Multiple MFEs on the same page share a single page client. The library tracks how many consumers are using it across all MFEs — when the last one unmounts, the page client is cleaned up automatically.
|
|
26
|
+
|
|
27
|
+
### Per-query option
|
|
28
|
+
|
|
29
|
+
Set `globalClient: 'page'` on an individual query:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// Composable
|
|
33
|
+
jobs = query<Job[]>(() => ({
|
|
34
|
+
queryKey: ['scheduling', 'jobs'],
|
|
35
|
+
queryFn: () => this.api?.getJobs(),
|
|
36
|
+
globalClient: 'page',
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Inheritance
|
|
40
|
+
get queryOptions(): QueryApiOptions<Job[]> {
|
|
41
|
+
return {
|
|
42
|
+
queryKey: ['scheduling', 'jobs'],
|
|
43
|
+
queryFn: () => this.api?.getJobs(),
|
|
44
|
+
globalClient: 'page',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## App-Level Client
|
|
50
|
+
|
|
51
|
+
Persists until the browser is refreshed or closed. Use cautiously — cached data stays active across all navigation.
|
|
52
|
+
|
|
53
|
+
Set `globalClient: 'app'` on an individual query:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
jobs = query<Job[]>(() => ({
|
|
57
|
+
queryKey: ['scheduling', 'jobs'],
|
|
58
|
+
queryFn: () => this.api?.getJobs(),
|
|
59
|
+
globalClient: 'app',
|
|
60
|
+
}));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Unlike the page client, there is no `getQueryClient('app')` provider. App-level sharing is set per-query using the `globalClient` option.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: QueryClientStore
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# QueryClientStore
|
|
6
|
+
|
|
7
|
+
The `QueryClientStore` manages the underlying TanStack `QueryClient` instance. The `@queryStore` decorator automatically injects it and wires it into every `QueryApi` and `MutationApi` instance, so most stores don't need to reference it directly.
|
|
8
|
+
|
|
9
|
+
Use it when you need to manipulate the query cache outside of a specific query — invalidating keys, fetching data imperatively, or cancelling requests.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Register it in your provider via `getQueryClient()`:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { getQueryClient } from '@servicetitan/tanstack-query-mobx/composable';
|
|
17
|
+
|
|
18
|
+
export const App: FC = provide({
|
|
19
|
+
singletons: [getQueryClient()],
|
|
20
|
+
})(observer(() => <YourApp />));
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
To override the defaults, pass a `QueryClientConfig` object:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { getQueryClient } from '@servicetitan/tanstack-query-mobx/composable';
|
|
27
|
+
|
|
28
|
+
export const App: FC = provide({
|
|
29
|
+
singletons: [
|
|
30
|
+
getQueryClient({
|
|
31
|
+
defaultOptions: {
|
|
32
|
+
queries: { staleTime: 5 * 60 * 1000, retry: 1 },
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
})(observer(() => <YourApp />));
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Custom config is only supported for local (component-scoped) clients. Shared clients (`'page'`, `'app'`) use consistent defaults across all MFEs — see [Shared Clients](./04-shared-clients).
|
|
40
|
+
|
|
41
|
+
## Defaults
|
|
42
|
+
|
|
43
|
+
The `QueryClientStore` creates a `QueryClient` with these defaults:
|
|
44
|
+
|
|
45
|
+
| Option | Default | Description |
|
|
46
|
+
|--------|---------|-------------|
|
|
47
|
+
| `staleTime` | 10 minutes | How long data is considered fresh before refetch |
|
|
48
|
+
| `refetchOnWindowFocus` | `false` | No automatic refetch when the browser tab regains focus |
|
|
49
|
+
| `refetchOnReconnect` | `false` | No automatic refetch when the network reconnects |
|
|
50
|
+
| `retry` | `3` | Number of retry attempts with exponential backoff |
|
|
51
|
+
| `gcTime` | 5 minutes (TanStack default) | Time before inactive queries are garbage collected |
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
All key-based methods (`invalidate`, `cancel`, `remove`) use **prefix matching** — `['scheduling']` matches all queries starting with that key, like `['scheduling', 'jobs']` and `['scheduling', 'stats']`. This is TanStack Query's default behavior.
|
|
56
|
+
|
|
57
|
+
### invalidate(keys, options?)
|
|
58
|
+
|
|
59
|
+
Invalidates queries across **all** available clients (local, page, and app). Invalidated queries are marked stale and refetch automatically when an observer is subscribed.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// Single key
|
|
63
|
+
queryClientStore.invalidate(['scheduling', 'jobs']);
|
|
64
|
+
|
|
65
|
+
// Multiple keys
|
|
66
|
+
queryClientStore.invalidate([['scheduling', 'jobs'], ['scheduling', 'stats']]);
|
|
67
|
+
|
|
68
|
+
// With deduplication — skip if currently in an invalidated state
|
|
69
|
+
queryClientStore.invalidate(['scheduling', 'jobs'], { dedupe: true });
|
|
70
|
+
|
|
71
|
+
// With a different invalidation key (invalidate a broader prefix)
|
|
72
|
+
queryClientStore.invalidate(['scheduling', 'jobs', 42], {
|
|
73
|
+
invalidationKey: ['scheduling', 'jobs'],
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Options:**
|
|
78
|
+
|
|
79
|
+
| Option | Type | Description |
|
|
80
|
+
|--------|------|-------------|
|
|
81
|
+
| `dedupe` | `boolean` | Skip if the query is currently in an invalidated state (hasn't refetched yet) |
|
|
82
|
+
| `invalidationKey` | `QueryKey` | Use a different key for invalidation than the one provided |
|
|
83
|
+
|
|
84
|
+
### fetchQuery(queryConfig, options?)
|
|
85
|
+
|
|
86
|
+
Fetches a query imperatively without subscribing to it. Respects caching and `staleTime` — if fresh data is already cached, it returns that instead of making a network request. Returns a deep clone of the data.
|
|
87
|
+
|
|
88
|
+
The query will retry according to the client's retry settings (default: 3 attempts with exponential backoff) before throwing. **Wrap calls in try/catch** — errors are thrown, not swallowed.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
try {
|
|
92
|
+
const report = await queryClientStore.fetchQuery({
|
|
93
|
+
queryKey: ['reporting', 'report', reportId],
|
|
94
|
+
queryFn: async () => (await api.getReport(reportId)).data,
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Called after all retries are exhausted
|
|
98
|
+
console.error('Failed to fetch report:', error);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Useful query config options (first parameter):**
|
|
103
|
+
|
|
104
|
+
| Option | Type | Description |
|
|
105
|
+
|--------|------|-------------|
|
|
106
|
+
| `staleTime` | `number` | Override stale time for this fetch. Set to `0` to force a fresh network request even if cached data exists |
|
|
107
|
+
| `retry` | `number \| boolean` | Override retry count for this specific fetch |
|
|
108
|
+
| `gcTime` | `number` | Override garbage collection time for this query |
|
|
109
|
+
|
|
110
|
+
**Second parameter options:**
|
|
111
|
+
|
|
112
|
+
| Option | Type | Description |
|
|
113
|
+
|--------|------|-------------|
|
|
114
|
+
| `appClient` | `boolean` | Fetch from the app-level shared client instead of the current `QueryClientStore` instance |
|
|
115
|
+
|
|
116
|
+
### cancel(keys, options?)
|
|
117
|
+
|
|
118
|
+
Cancels in-flight requests for queries matching the provided key(s).
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
queryClientStore.cancel(['scheduling', 'jobs']);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Second parameter options:**
|
|
125
|
+
|
|
126
|
+
| Option | Type | Description |
|
|
127
|
+
|--------|------|-------------|
|
|
128
|
+
| `appClient` | `boolean` | Cancel on the app-level shared client instead of the current `QueryClientStore` instance |
|
|
129
|
+
|
|
130
|
+
### remove(keys)
|
|
131
|
+
|
|
132
|
+
Removes queries from the cache entirely — the query, its data, and all metadata are deleted. Unlike `invalidate`, this does **not** trigger a refetch.
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
queryClientStore.remove(['old', 'data']);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### queryClient
|
|
139
|
+
|
|
140
|
+
This offers direct access to the underlying TanStack `QueryClient` instance. Use it for advanced TanStack Query options not exposed by the methods above — for example, exact key matching:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
// Invalidate only ['scheduling', 'jobs', 42], not ['scheduling', 'jobs', 43]
|
|
144
|
+
queryClientStore.queryClient.invalidateQueries({
|
|
145
|
+
queryKey: ['scheduling', 'jobs', 42],
|
|
146
|
+
exact: true,
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## When to Use Directly
|
|
151
|
+
|
|
152
|
+
Most stores don't need to inject `QueryClientStore` — each query instance already has its own `invalidate()` method (e.g., `this.jobs.invalidate()`), and the `@queryStore` decorator handles setup and disposal automatically. Inject `QueryClientStore` directly when you need to:
|
|
153
|
+
|
|
154
|
+
- **Invalidate from outside a query** — e.g., a mutation in one store needs to invalidate a query owned by a different store, using a shared key prefix
|
|
155
|
+
- **Fetch data imperatively** — one-off fetch without setting up a full query subscription
|
|
156
|
+
- **Cancel or remove queries** — clean up specific cache entries on demand
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Testing
|
|
6
|
+
|
|
7
|
+
Use `ContainerBuilder` from `@servicetitan/tanstack-query-mobx/test` to set up stores in tests. It handles `QueryClientStore` setup, store initialization, and waits for all queries to settle.
|
|
8
|
+
|
|
9
|
+
## Quick Example
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { ContainerBuilder } from '@servicetitan/tanstack-query-mobx/test';
|
|
13
|
+
|
|
14
|
+
const { container, initialize } = new ContainerBuilder()
|
|
15
|
+
.add(JobsStore)
|
|
16
|
+
.add(JobsApi)
|
|
17
|
+
.build();
|
|
18
|
+
|
|
19
|
+
jest.spyOn(container.get(JobsApi), 'getJobs').mockResolvedValue({
|
|
20
|
+
data: [{ id: 1, title: 'Plumbing' }],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await initialize();
|
|
24
|
+
expect(container.get(JobsStore).jobs.data).toHaveLength(1);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Test Defaults
|
|
28
|
+
|
|
29
|
+
`ContainerBuilder` creates a `QueryClientStore` with test-friendly defaults:
|
|
30
|
+
- **`staleTime: Infinity`** — queries never go stale, refetches only on explicit invalidation
|
|
31
|
+
- **`retry: false`** — failed queries don't retry, test failures surface immediately
|
|
32
|
+
|
|
33
|
+
## Key Features
|
|
34
|
+
|
|
35
|
+
- **`skipQueries` option** — skip waiting for specific queries (required for `enabled: false` queries that should stay disabled): `await initialize({ skipQueries: [store.details] })`
|
|
36
|
+
- **`ignoreStores` option** — skip entire stores: `await initialize({ ignoreStores: [OtherStore] })`
|
|
37
|
+
- **Mock values** — `builder.add(JobsApi, { useValue: mockApi })` for mock dependencies
|
|
38
|
+
- **`waitFor` helper** — wait for MobX observable conditions: `await waitFor(() => store.items.initialized)`
|
|
39
|
+
|
|
40
|
+
## Stores with Global Clients
|
|
41
|
+
|
|
42
|
+
Queries using `globalClient` create shared instances on `window`, which breaks test isolation. Use `useClass` to swap in a local version that strips `globalClient`, or `useValue` to mock the store entirely:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// useClass — keeps real store logic, removes global client
|
|
46
|
+
@queryStore
|
|
47
|
+
class JobsLocalStore extends JobsStore {
|
|
48
|
+
@override
|
|
49
|
+
get jobsOptions(): QueryApiOptions<Job[]> {
|
|
50
|
+
return { ...super.jobsOptions, globalClient: undefined };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { initialize } = new ContainerBuilder()
|
|
55
|
+
.add(JobsStore, { useClass: JobsLocalStore })
|
|
56
|
+
.build();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// useValue — mock the store shape directly
|
|
61
|
+
const { initialize } = new ContainerBuilder()
|
|
62
|
+
.add(JobsStore, { useValue: { jobs: { data: mockJobs } } })
|
|
63
|
+
.build();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Full Testing Guide
|
|
67
|
+
|
|
68
|
+
See the complete [Testing Guide](https://github.com/servicetitan/anvil-uikit-contrib/blob/master/packages/tanstack-query-mobx/src/test/TESTING.md) for:
|
|
69
|
+
- `ContainerBuilder` full API
|
|
70
|
+
- Skipping queries and disabled query handling
|
|
71
|
+
- Queries dependent on external data
|
|
72
|
+
- Mock values and `useClass` / `useValue` patterns
|
|
73
|
+
- Manual setup without `ContainerBuilder`
|
|
74
|
+
- Stores with global clients
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
2
|
+
title: Examples
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -8,36 +8,32 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import { CodeDemo } from '@site/src/components/code-demo';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Interactive Examples
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Examples
|
|
16
|
-
|
|
17
|
-
### App QueryClient
|
|
13
|
+
## App QueryClient
|
|
18
14
|
|
|
19
15
|
<CodeDemo example={AppExample} srcPath="tanstack-query-mobx/src/demo/app-example.tsx" />
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
## Page QueryClient
|
|
22
18
|
|
|
23
19
|
<CodeDemo example={PageExample} srcPath="tanstack-query-mobx/src/demo/page-example.tsx" />
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
## Initial Data
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
### No Initial Data
|
|
28
24
|
|
|
29
25
|
<CodeDemo example={NoInitialDataExample} srcPath="tanstack-query-mobx/src/demo/no-initial-data-example.tsx" />
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
### Has Initial Data
|
|
32
28
|
|
|
33
29
|
<CodeDemo example={HasInitialDataExample} srcPath="tanstack-query-mobx/src/demo/has-initial-data-example.tsx" />
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
## Stale Time
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
### Stale Time: 0
|
|
38
34
|
|
|
39
35
|
<CodeDemo example={StaleTime0Example} srcPath="tanstack-query-mobx/src/demo/stale-time-0-example.tsx" />
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
### Stale Time: 10 Minutes
|
|
42
38
|
|
|
43
39
|
<CodeDemo example={StaleTime10MinutesExample} srcPath="tanstack-query-mobx/src/demo/stale-time-10-minutes-example.tsx" />
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: TanStack Query MobX
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# TanStack Query MobX Integration
|
|
6
|
+
|
|
7
|
+
`@servicetitan/tanstack-query-mobx` integrates [TanStack Query](https://tanstack.com/query/latest) with MobX stores and `@servicetitan/react-ioc` dependency injection. It provides declarative data fetching, caching, automatic query deduplication, and observable state management.
|
|
8
|
+
|
|
9
|
+
TanStack Query does not provide a query mechanism — you supply an async function using fetch, Axios, or the generated API functions. TanStack Query handles caching, retries, deduplication, refetching, and state.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @servicetitan/tanstack-query-mobx
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### 1. Provide the Query Client
|
|
20
|
+
|
|
21
|
+
A `QueryClientStore` is required at the top store level. It creates the TanStack `QueryClient` with sensible defaults (10-minute stale time, 3 retries, no refetch on window focus).
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { getQueryClient } from '@servicetitan/tanstack-query-mobx/composable';
|
|
25
|
+
|
|
26
|
+
export const App: FC = provide({
|
|
27
|
+
singletons: [getQueryClient()],
|
|
28
|
+
})(observer(() => <YourApp />));
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Create a Store
|
|
32
|
+
|
|
33
|
+
Two approaches — both use the same query client.
|
|
34
|
+
|
|
35
|
+
#### Composable with `@queryStore` (recommended)
|
|
36
|
+
|
|
37
|
+
The `@queryStore` decorator + `query()`/`mutation()` factories. All queries are class fields with consistent access patterns. Favors composition over inheritance.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { queryStore, query, mutation } from '@servicetitan/tanstack-query-mobx/composable';
|
|
41
|
+
|
|
42
|
+
@queryStore
|
|
43
|
+
class JobsStore extends Store {
|
|
44
|
+
@inject(JobsApi) private api?: JobsApi;
|
|
45
|
+
@observable selectedJobId = 0;
|
|
46
|
+
|
|
47
|
+
jobs = query<Job[]>(() => ({
|
|
48
|
+
queryKey: ['scheduling', 'jobs'],
|
|
49
|
+
queryFn: async () => (await this.api?.getJobs())?.data ?? [],
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
jobDetails = query<JobDetails>(() => ({
|
|
53
|
+
queryKey: ['scheduling', 'job', this.selectedJobId],
|
|
54
|
+
queryFn: async () => (await this.api?.getJob(this.selectedJobId))?.data,
|
|
55
|
+
enabled: this.selectedJobId > 0,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
deleteJob = mutation<void, { id: number }>(() => ({
|
|
59
|
+
mutationFn: async (arg) => await this.api?.deleteJob(arg.id),
|
|
60
|
+
invalidatedQueries: [['scheduling', 'jobs']],
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Why `extends Store`?** Stores must extend `Store` from `@servicetitan/react-ioc` to hook into the react-ioc lifecycle — `initialize()` runs when the provider mounts, `dispose()` runs when it unmounts. The `@queryStore` decorator uses these hooks to auto-wire queries and clean up subscriptions.
|
|
66
|
+
|
|
67
|
+
**What `@queryStore` handles automatically:**
|
|
68
|
+
- Applies `@injectable()` (no separate decorator needed)
|
|
69
|
+
- Injects `QueryClientStore` (no `@inject(QueryClientStore)` needed)
|
|
70
|
+
- Auto-discovers `QueryApi` and `MutationApi` properties and wires `setup()`/`dispose()`
|
|
71
|
+
- Chains consumer `initialize()` and `dispose()` methods
|
|
72
|
+
- Safe with deep inheritance — prevents double setup/dispose
|
|
73
|
+
|
|
74
|
+
#### Inheritance with `QueryApiStore`
|
|
75
|
+
|
|
76
|
+
Extend `QueryApiStore<T>` with a primary query via `queryOptions` getter, plus optional secondary queries via `addQuery()`.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { QueryApiStore, QueryApiOptions } from '@servicetitan/tanstack-query-mobx';
|
|
80
|
+
|
|
81
|
+
@injectable()
|
|
82
|
+
class JobsStore extends QueryApiStore<Job[]> {
|
|
83
|
+
@inject(JobsApi) private api?: JobsApi;
|
|
84
|
+
|
|
85
|
+
get queryOptions(): QueryApiOptions<Job[]> {
|
|
86
|
+
return {
|
|
87
|
+
queryKey: ['scheduling', 'jobs'],
|
|
88
|
+
queryFn: async () => (await this.api?.getJobs())?.data ?? [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
deleteJob = this.addMutation<void, { id: number }>(() => ({
|
|
93
|
+
mutationFn: async (arg) => await this.api?.deleteJob(arg.id),
|
|
94
|
+
invalidatedQueries: [['scheduling', 'jobs']],
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. Use in Components
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
const [store] = useDependencies(JobsStore);
|
|
103
|
+
|
|
104
|
+
// Composable — access via named query
|
|
105
|
+
const { data, isLoading } = store.jobs;
|
|
106
|
+
|
|
107
|
+
// Inheritance — primary query proxied to store
|
|
108
|
+
const { data, isLoading } = store;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## How It Works
|
|
112
|
+
|
|
113
|
+
TanStack Query's main engine is the **query client**. Stores subscribe to the client using composite keys. When a store initializes, it subscribes its queries to the client. When it disposes, it unsubscribes.
|
|
114
|
+
|
|
115
|
+
- **Queries** are data-driven — they run automatically based on their options. The developer doesn't call them directly. Changes to `queryKey` values trigger refetches via MobX reactivity.
|
|
116
|
+
- **Mutations** are called manually via `runMutation()`. They can automatically invalidate (refetch) related queries after success.
|
|
117
|
+
- **Caching** is per query key. Multiple stores with the same key share one cache and one network request (deduplication).
|
|
118
|
+
|
|
119
|
+
The `QueryClientStore` creates and manages the TanStack `QueryClient`. The `@queryStore` decorator (or `QueryApiStore` base class) subscribes queries to this client during the react-ioc `initialize()` lifecycle and unsubscribes during `dispose()`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/docs-anvil-uikit-contrib",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "39.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,5 +16,5 @@
|
|
|
16
16
|
"cli": {
|
|
17
17
|
"webpack": false
|
|
18
18
|
},
|
|
19
|
-
"gitHead": "
|
|
19
|
+
"gitHead": "383b103c240822865df56f9664f6dcfd0397f122"
|
|
20
20
|
}
|