You could get used to this: Managing GraphQL data
Ember.js Berlin - July 13th, 2021
Francesco Novy
@_fnovy
hello@fnovy.com
mydea
Our stack
- Ember (Octane)
- GraphQL API (Java)
- Cloud Connector (on-premise, C#)
Ember & GraphQL
Sneak peak: GraphQL
# app/gql/queries/user-by-email.graphql
query userByEmail($email: String!) {
userByEmail(email: $email) {
id
email
name
roles {
id
name
}
}
}
import { queryManager } from 'ember-apollo-client';
import query from 'my-app/gql/queries/user-by-email.graphql';
export default class MyService extends Service {
@queryManager apollo;
getUserForEmail(email) {
let variables = { email: 'francesco@fabscale.com' };
return this.apollo.query({ query, variables }, 'userByEmail');
}
}
{
id: '1',
email: 'francesco@fabscale.com',
name: 'Francesco Novy',
roles: [
{
id: '1',
name: 'Admin',
},
{
id: '6',
name: 'Maintenance manager',
},
],
}
Apollo caching
-
Apollo allows to cache full requests
-
The idea is good, but...
- ... manually keeping track of changes & ensuring no stale state remains is tricky.
Example for caching behavior
import query from 'my-app/gql/queries/user-by-email.graphql';
// Will hit the network
let user = await this.apollo.query({
query,
variables: { email: 'francesco@fabscale.com'}
}, 'userByEmail');
// Will not hit the network, but re-use the previous response
let user2 = await this.apollo.query({
query,
variables: { email: 'francesco@fabscale.com'}
}, 'userByEmail');
// Will hit the network, as variables are different
let user = await this.apollo.query({
query,
variables: { email: 'anne@fabscale.com'}
}, 'userByEmail');
Cache & mutations are tricky
import query from 'my-app/gql/queries/user-by-email.graphql';
import mutation from 'my-app/gql/mutations/update-user.graphql';
let user = await this.apollo.query({
query,
variables: { email: 'francesco@fabscale.com'}
}, 'userByEmail');
await this.apollo.mutate({
mutation,
variables: { email: 'francesco@fabscale.com', name: 'Francesco Smith' }
}, 'updateUser');
// Will not hit the network, but re-use the previous response
let user2 = await this.apollo.query({
query,
variables: { email: 'francesco@fabscale.com'}
}, 'userByEmail');
// user2.name --> still "Francesco Novy"
Cache & mutations are tricky (2)
- You can manually update the cache after mutations
- However, this can become tricky when dealing with relationships, list & single results, etc.
Our solution: A custom cache
- We leverage `fetchPolicy` to force Apollo to make new queries when necessary
- We keep a custom cache list for each entity type
- We keep track of when a query was made and invalidate it after a given time
- We do not manually update any caches, but only cache/invalidate full queries
Usage - queries
import usersQuery from 'fabscale-app/gql/queries/users.graphql';
import userByEmailQuery from 'fabscale-app/gql/queries/user-by-email.graphql';
export default class UserStore extends Service {
@service storeManager;
loadUsers() {
return this.storeManager.query({
query: usersQuery,
variables: {}
}, {
namespace: 'users',
cacheEntity: 'User',
cacheSeconds: 300
});
}
loadUser(email) {
return this.storeManager.query({
query: userByEmailQuery,
variables: { email }
}, {
namespace: 'userByEmail',
cacheEntity: 'User'
cacheId: email,
cacheSeconds: 500
});
}
}
Usage - mutations
import editUserMutation from 'fabscale-app/gql/mutations/edit-user.graphql';
export default class UserStore extends Service {
// ...
async editUser(email, name) {
return await this.storeManager.mutate({
mutation: editUserMutation,
variables: { email, name }
}, {
namespace: 'editUser',
invalidateCache: [
{
cacheEntity: 'User',
},
{
cacheEntity: 'User',
cacheId: email,
},
],
});
}
}
How it works?
export default class StoreManagerService extends Service {
@queryManager apollo;
query(
options,
{
namespace,
cacheEntity,
cacheId,
cacheSeconds = 60,
} = {}
) {
let useCache = this._checkShouldUseCache(options, {
cacheEntity,
cacheId,
cacheSeconds,
});
// Generally, use `cache-first` as strategy, to avoid repeated lookups
// However, if the last query was longer ago than the specified `cacheSeconds`,
// use `network-only` to force re-fetching it
let fetchPolicy = useCache ? 'cache-first' : 'network-only';
return this.apollo.query(
Object.assign({ fetchPolicy }, options),
namespace
);
}
}
Checking the cache
export default class StoreManagerService extends Service {
_checkShouldUseCache({ cacheEntity, cacheId, cacheSeconds }) {
let { storeCache } = this;
let cache = storeCache.getCache(cacheEntity);
let lastLookup = cacheId ? cache.getForId(cacheId) : cache.getOverall();
let now = +new Date() / 1000;
// If the last actual query was longer ago than the specified `cacheSeconds`
// If `cacheSeconds` is 0, we always want to re-fetch
// If no lookup was made yet, we always want to re-fetch
let lastLookupIsOld =
!cacheSeconds || !lastLookup || lastLookup < now - cacheSeconds;
// We only update the last lookup time if we are re-fetching
// To avoid the next lookup being pushed out indefinitely
if (lastLookupIsOld) {
if (cacheId) {
cache.setForId(cacheId, now);
} else {
cache.setOverall(now);
}
}
return !lastLookupIsOld;
}
Get used to this?
Resources for data loading
import { Resource } from 'ember-could-get-used-to-this';
// app/helpers/resources/load-user.js
export default class LoadUserResource extends Resource {
@service userStore;
@tracked isError = false;
@tracked error;
@tracked data;
get value() {
let isLoading = this.loadDataTask.isRunning;
let { isError } = this;
let data = isError ? this.error : this.data;
return {
isLoading,
data,
isError,
};
}
// ...
}
Resources for data loading (2)
export default class LoadUserResource extends Resource {
// ...
setup() {
let [email] = this.args.positional;
this.loadDataTask.perform(options);
}
teardown() {
this.loadDataTask.cancelAll();
}
@restartableTask
*loadDataTask(email) {
this.isError = false;
try {
this.data = yield this.userStore.loadUser(email);
} catch (error) {
this.error = error.message;
this.isError = true;
}
}
}
Using resources
{{#let
(resources/load-user 'francesco@fabscale.com')
as |resource|
}}
{{#if resource.isLoading}}
Loading...
{{else if resource.isError}}
Error: {{resource.data}}
{{else}}
Your name: {{resource.data.name}}
{{/if}}
{{/let}}
Thank you!
@_fnovy
hello@fnovy.com
mydea
You could get used to this: Managing GraphQL data
By Francesco Novy
You could get used to this: Managing GraphQL data
- 379