Manually update any components rendering that query. This is useful if you (dangerously) update a query's cache, as discussed in the caching section, below
74 |
75 |
76 |
subscribeMutation({ when, run })
77 |
Manually subscribe to a mutation, in order to manually update the cache. See below for more info
78 |
79 |
80 |
81 |
82 | ## Running queries and mutations
83 |
84 | ### Preloading queries
85 |
86 | It's always a good idea to preload a query as soon as you know it'll be requested downstream by a (possibly lazy loaded) component. To preload a query, call the `preload` method on the client, and pass a query, and any args you might have.
87 |
88 | ```javascript
89 | import { getDefaultClient } from "micro-graphql-svelte";
90 |
91 | const client = getDefaultClient();
92 | client.preload(YourQuery, variables);
93 | ```
94 |
95 | ### Queries
96 |
97 | ```js
98 | import { query } from "micro-graphql-svelte";
99 |
100 | let { queryState, resultsState, sync } = query(YOUR_QUERY);
101 | $: booksSync($searchState);
102 | ```
103 |
104 | `query` takes the following arguments
105 |
106 |
107 | | Arg | Description |
108 | | -------| ----------- |
109 | | `query: string` | The query text |
110 | | `options: object` | The query's options (optional) |
111 |
112 | The options argument, if supplied, can contain these properties
113 |
114 |
115 | | Option | Description |
116 | | -------| ----------- |
117 | | `onMutation` | A map of mutations, along with handlers. This is how you update your cached results after mutations, and is explained more fully below |
118 | | `client` | Manually pass in a client to be used for this query, which will override the default client|
119 | | `cache` | Manually pass in a cache object to be used for this query, which will override the default cache|
120 | | `initialSearch` | If you'd like to run a query immediately, without calling the returned sync method, provide the arguments object here.|
121 | | `activate` | Optional function that will run whenever the query becomes active, in other words the query store is subscribed by at least one component, or manual call to `.subscribe`.|
122 | | `deactivate` | Optional function that will run whenever the query becomes in-active, in other words the query store is not subscribed by any components, or manual calls to `.subscribe`.|
123 | | `postProcess` | An optional function to run on new search results. You can perform side effects in here, ie preloading images, and optionally return new results, which will then become the results for this query. If you return nothing, the original results will be used. |
124 |
125 | Be sure to use the `compress` tag to remove un-needed whitespace from your query text, since it will be sent via HTTP GET—for more information, see [here](./compress). An even better option would be to use my [persisted queries helper](https://github.com/arackaf/generic-persistgraphql). This not only removes the entire query text from your network requests altogether, but also from your bundled code.
126 |
127 | `query` returns an object with the following properties: `queryState`, a store with your query's current results; a `sync` function, which you can call anytime to update the query's current variables; and `resultsState`, a store with just the current query's actual data results—in other words a straight projection of `$queryState.data`. Your query will not actually run until you've called sync. If your query does not need any variables, just call it immediately with an empty object, or supply an `initialSearch` value (see above).
128 |
129 | If you need to run code when the current query's results (ie `data`), and **only** the current query's results changes, use the `resultsState` store in your component, since reactive blocks referencing `$resultsState` will frequently fire even when `data` has not changed, for example when `loading` has been set to true.
130 |
131 | ### Query results
132 |
133 | The `queryState` store has the following properties
134 |
135 |
136 | | Props | Description |
137 | | ----- | ----------- |
138 | |`loading`|Fetch is executing for your query|
139 | |`loaded`|Fetch has finished executing for your query|
140 | |`data`|If the last fetch finished successfully, this will contain the data returned, else null|
141 | |`currentQuery`|The query that was run, which produced the current results. This updates synchronously with updates to `data`, so you can use changes here as an easy way to subscribe to query result changes. This will not have a value until there are results passed to `data`. In other words, changes to `loading` do not affect this value|
142 | |`error`|If the last fetch did not finish successfully, this will contain the errors that were returned, else `null`|
143 | |`reload`|`function`: Manually re-fetches the current query|
144 | |`clearCache`|`function`: Clear the cache for this query|
145 | | `softReset` |`function`: Clears the cache, but does **not** re-issue any queries. It can optionally take an argument of new, updated results, which will replace the current `data` props |
146 | | `hardReset` |`function`: Clears the cache, and re-load the current query from the network|
147 |
148 | ### Mutations
149 |
150 | ```js
151 | import { mutation } from "micro-graphql-svelte";
152 |
153 | const { mutationState } = mutation(YOUR_MUTATION);
154 | ```
155 |
156 | The mutation function takes the following arguments.
157 |
158 |
159 | | Arg | Description |
160 | | ------------- | --------- |
161 | | `mutation: string` | Mutation text |
162 | | `options: object` | Mutation options (optional) |
163 |
164 | The options argument, if supplied, can contain this property
165 |
166 |
167 | | Option | Description |
168 | | ------------- | --------- |
169 | | `client` | Override the client used |
170 |
171 | ### Mutation results
172 |
173 | `mutation` returns a store with the following properties.
174 |
175 |
176 | | Option | Description |
177 | | ------------- | --------- |
178 | | `running` | Mutation is executing |
179 | | `finished` | Mutation has finished executing|
180 | | `runMutation` | A function you can call when you want to run your mutation. Pass it your variables |
181 |
182 | ## Caching
183 |
184 | The client object maintains a cache of each query it comes across when processing your components. The cache is LRU with a default size of 10 and, again, stored at the level of each specific query, not the GraphQL type. As your instances mount and unmount, and update, the cache will be checked for existing results to matching queries.
185 |
186 | ### Cache object
187 |
188 | You can import the `Cache` class like this
189 |
190 | ```javascript
191 | import { Cache } from "micro-graphql-svelte";
192 | ```
193 |
194 | When instantiating a new cache object, you can optionally pass in a cache size.
195 |
196 | ```javascript
197 | let cache = new Cache(15);
198 | ```
199 |
200 | #### Cache api
201 |
202 | The cache object has the following properties and methods
203 |
204 |
205 | | Member | Description |
206 | | ----- | --------- |
207 | | `get entries()` | An array of the current entries. Each entry is an array of length 2, of the form `[key, value]`. The cache entry key is the actual GraphQL url query that was run. If you'd like to inspect it, see the variables that were sent, etc, just use your favorite url parsing utility, like `url-parse`. And of course the cache value itself is whatever the server sent back for that query. If the query is still pending, then the entry will be a promise for that request. |
208 | | `get(key)` | Gets the cache entry for a particular key |
209 | | `set(key, value)` | Sets the cache entry for a particular key |
210 | | `delete(key)` | Deletes the cache entry for a particular key |
211 | | `clearCache()` | Clears all entries from the cache |
212 |
213 | ### Cache invalidation
214 |
215 | The onMutation option that query options take is an object, or array of objects of the form `{ when: string|regularExpression, run: function }`
216 |
217 | `when` is a string or regular expression that's tested against each result of any mutations that finish. If the mutation has any result set names that match, `run` will be called with three arguments: an object with these properties, described below, `{ softReset, currentResults, hardReset, cache, refresh }`; the entire mutation result; and the mutation's `variables` object.
218 |
219 |
220 | | Arg | Description |
221 | | ---| -------- |
222 | | `softReset` | Clears the cache, but does **not** re-issue any queries. It can optionally take an argument of new, updated results, which will replace the current `data` props |
223 | | `currentResults` | The current results that are passed as your `data` prop |
224 | | `hardReset` | Clears the cache, and re-load the current query from the network|
225 | | `cache` | The actual cache object. You can enumerate its entries, and update whatever you need|
226 | | `refresh` | Refreshes the current query, from cache if present. You'll likely want to call this after modifying the cache |
227 |
228 | Many use cases follow. They're based on a hypothetical book tracking website.
229 |
230 | The code below uses a publicly available GraphQL endpoint created by my [mongo-graphql-starter project](https://github.com/arackaf/mongo-graphql-starter). You can run these examples from the demo folder of this repository. Just run `npm i` then run the `npm run demo` and `npm starte` scripts in separate terminals, and open `http://localhost:8082/`
231 |
232 | #### Hard Reset: Reload the query after any relevant mutation
233 |
234 | Let's say whenever a mutation happens, we want to immediately invalidate any related queries' caches, and reload the current data from the network. We understand this may cause a book we just edited to immediately disappear from our current search results, since it no longer matches our search criteria.
235 |
236 | The `hardReset` method that's passed makes this easy. Let's see how to use this in a (contrived) component that queries, and displays some books and subjects.
237 |
238 | ```svelte
239 |
257 |
258 |
259 | ```
260 |
261 | Here we specify a regex matching every kind of book, or subject mutation, and upon completion, we just clear the cache, and reload by calling `hardReset()`. It's hard not to be a littler dissatisfied with this solution; the boilerplate is non-trivial.
262 |
263 | Assuming our GraphQL operations have a consistent naming structure—and they should, and in this case do—then some pretty obvious patterns emerge. We can write some basic helpers to remove some of this boilerplate.
264 |
265 | ```javascript
266 | //hardResetHelpers.js
267 | import { query } from "micro-graphql-svelte";
268 |
269 | export const hardResetQuery = (type, queryToRun, options = {}) =>
270 | query(queryToRun, {
271 | ...options,
272 | onMutation: {
273 | when: new RegExp(`(update|create|delete)${type}s?`),
274 | run: ({ hardReset }) => hardReset()
275 | }
276 | });
277 | ```
278 |
279 | which we _could_ use like this
280 |
281 | ```svelte
282 |
296 |
297 |
298 | ```
299 |
300 | but really, why not go the extra mile and make wrappers for our various types, like so
301 |
302 | ```javascript
303 | //hardResetHelpers.js
304 | import { query } from "micro-graphql-svelte";
305 |
306 | export const hardResetQuery = (type, queryToRun, options = {}) =>
307 | query(queryToRun, {
308 | ...options,
309 | onMutation: {
310 | when: new RegExp(`(update|create|delete)${type}s?`),
311 | run: ({ hardReset }) => hardReset()
312 | }
313 | });
314 |
315 | export const bookHardResetQuery = (...args) => hardResetQuery("Book", ...args);
316 | export const subjectHardResetQuery = (...args) => hardResetQuery("Subject", ...args);
317 | ```
318 |
319 | which simplifies the code to just this
320 |
321 | ```svelte
322 |
336 |
337 |
338 | ```
339 |
340 | #### Soft Reset: Update current results, but clear the cache
341 |
342 | Assume that after a mutation you want to update your current results based on what was changed, clear all cache entries, including the existing one, but **not** run any network requests. So if you're currently searching for an author of Dumas Malone, but one of the current results was written by Shelby Foote, and you click the book's edit button and fix it, you want that book to now show the updated value, but stay in the current results, since re-loading the current query and having the book just vanish is bad UX in your opinion.
343 |
344 | Here's the same component from above, but with our new cache strategy
345 |
346 | ```svelte
347 |
385 |
386 |
387 |
388 | ```
389 |
390 | Whenever a mutation comes back with `updateBook` or `updateBooks` results, we manually update our current results, then call `softReset`, which clears our cache, including the current cache result; so if you page up, then come back down to where you were, a **new** network request will be run, and your edited books may no longer be there, if they no longer match the search results. And likewise for subjects.
391 |
392 | Obviously this is more boilerplate than we'd ever want to write in practice, so let's tuck it behind a helper, like we did before.
393 |
394 | ```javascript
395 | //softResetHelpers.js
396 | import { query } from "micro-graphql-svelte";
397 |
398 | export const softResetQuery = (type, queryToUse, options = {}) =>
399 | query(queryToUse, {
400 | ...options,
401 | onMutation: {
402 | when: new RegExp(`update${type}s?`),
403 | run: ({ softReset, currentResults }, resp) => {
404 | const updatedItems = resp[`update${type}s`]?.[`${type}s`] ?? [resp[`update${type}`][type]];
405 | updatedItems.forEach(updatedItem => {
406 | let CachedItem = currentResults[`all${type}s`][`${type}s`].find(item => item._id == updatedItem._id);
407 | CachedItem && Object.assign(CachedItem, updatedItem);
408 | });
409 | softReset(currentResults);
410 | }
411 | }
412 | });
413 |
414 | export const bookSoftResetQuery = (...args) => softResetQuery("Book", ...args);
415 | export const subjectSoftResetQuery = (...args) => softResetQuery("Subject", ...args);
416 | ```
417 |
418 | which we can use like this
419 |
420 | ```svelte
421 |
435 |
436 |
437 | ```
438 |
439 | #### Manually update all affected cache entries
440 |
441 | Let's say you want to intercept mutation results, and manually update your cache. This is difficult to get right, so be careful. You'll likely only want to do this with data that are not searched or filtered, but even then, softReset will likely be good enough.
442 |
443 | For this, we can call the `subscribeMutation` method on the client object, and pass in the same `when` test, and `run` callback as before. Except now the `run` callback will receive a `refreshActiveQueries` callback, which we can use to force any queries showing data from a particular query to update itself from the now-updated cache. This function returns a cleanup function which you can call to remove the subscription.
444 |
445 | The manual solution might look something like this
446 |
447 | ```javascript
448 | //cacheHelpers.js
449 | export const syncCollection = (current, newResultsLookup) => {
450 | return current.map(item => {
451 | const updatedItem = newResultsLookup.get(item._id);
452 | return updatedItem ? Object.assign({}, item, updatedItem) : item;
453 | });
454 | };
455 | ```
456 |
457 | and
458 |
459 | ```svelte
460 |
516 |
517 |
518 | ```
519 |
520 | It's a lot of code, but as always the idea is that you'd wrap it all into some re-usable helpers, like this
521 |
522 | ```javascript
523 | //cacheHelpers.js
524 | import { getDefaultClient } from "micro-graphql-svelte";
525 |
526 | export const syncCollection = (current, newResultsLookup) => {
527 | return current.map(item => {
528 | const updatedItem = newResultsLookup.get(item._id);
529 | return updatedItem ? Object.assign({}, item, updatedItem) : item;
530 | });
531 | };
532 |
533 | export const syncQueryToCache = (query, type) => {
534 | const graphQLClient = getDefaultClient();
535 | graphQLClient.subscribeMutation([
536 | {
537 | when: new RegExp(`update${type}s?`),
538 | run: ({ refreshActiveQueries }, resp, variables) => {
539 | const cache = graphQLClient.getCache(query);
540 | const newResults = resp[`update${type}`] ? [resp[`update${type}`][type]] : resp[`update${type}s`][`${type}s`];
541 | const newResultsLookup = new Map(newResults.map(item => [item._id, item]));
542 |
543 | for (let [uri, { data }] of cache.entries) {
544 | data[`all${type}s`][`${type}s`] = syncCollection(data[`all${type}s`][`${type}s`], newResultsLookup);
545 | }
546 |
547 | refreshActiveQueries(query);
548 | }
549 | }
550 | ]);
551 | };
552 | ```
553 |
554 | which cuts the usage code to just this
555 |
556 | ```svelte
557 |
575 |
576 |
577 | ```
578 |
579 | The above code assumes this component will only ever render once. If that's not the case, put these calls to `subscribeMutation` somewhere else, that will only ever run one. A svelte `
18 |
19 |
23 |