we're using apollo client in a web application and are looking for ways to improve cache usage.
We have a query that takes an array of ids as parameter an example query with ids foo and bar would look like this:
query routes {
routes(routeNames: ["foo", "bar"]) {
items {
name
route
defaults
}
}
}
The cache setup looks like this:
export const cacheRedirects = {
Query: {
routes: (_: any, args: RoutesArgs, { getCacheKey }: Resolver<'name'>): Array<CacheKey> =>
args.routeNames.map(name => getCacheKey({ __typename: 'Route', name })),
},
};
export const dataIdFromObject = (object: QueryResult): ?string => {
switch (object.__typename) {
case 'Route':
return `${object.__typename}:${object.name}`;
default: return defaultDataIdFromObject(object);
}
};
export function newCache(): InMemoryCache {
return new InMemoryCache({ dataIdFromObject, cacheRedirects });
}
Now when using the query in several places in our client we'd like to fetch only data for routeNames not cached via network and retrieve the rest via cache.
So the problem boils down to this:
When having one query that caches the results for routeNames: ["foo", "bar"] and later another query comes along asking for the routes for routeNames: ["bar", "baz"] we'd love to take the result corresponding to "bar" from cache and send a query for routeNames: ["baz"].
I'm uncertain whether and how this can be done with Apollo because in contrast to the cacheRedirect example here we deal with multiple ids rather than a single one.
Now if we can't cache per array item the next best thing we could do would be to transform the ids into common cache keys so that ["foo", "bar"] and ["bar", "foo"] end up using the same cache key, but ["foo", "baz"] would use a different one.
Of course the ideal thing would be to only fetch "baz" as the missing item in our scenario.
One idea is to check the local cache before making the actual query, and simply drop the IDs (routeNames) already in the cache from the list of IDs (routeNames) to query.
To check the cache without touching the network, use readQuery() or readFragment().
The documentation is here:
https://www.apollographql.com/docs/react/advanced/caching.html#readquery
https://www.apollographql.com/docs/react/advanced/caching.html#readfragment
UPDATE: Alternatively you can also set the fetchPolicy to cache-only in your query options.
https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-config-options-fetchPolicy
UPDATE 2: A nice way to integrate the checks so they run for every query may be a custom client middleware, which will be run on every request before calling the server.
https://www.apollographql.com/docs/react/advanced/network-layer.html#linkMiddleware
Related
I need to get a number of items from a GraphQL enabled database (no control over its schema) and output them in the exact order called.
For example, if the database holds the items 1,2,3 in that respective order I need to get them as 3,1,2.
Query:
{items(filter: {id: {_in: ["3","1","2"] } } ) {data}}
Actual result:
{"data": {"items": [{"data": "data-from-1"},{"data": "data-from-2"},{"data": "data-from-3"}]}}
Expected result:
{"data": {"items": [{"data": "data-from-3"},{"data": "data-from-1"},{"data": "data-from-1"}]}}
So I guess that what I'm looking for is a 'meta' operator that relates to other operators rather than the actual query – something like:
sort:["_in"] or orderby:{operator:"_in"}
...but I didn't manage to find out if such a thing exists or not.
So is it possible in general or maybe in some flavour of GraphQL? Or is it my only choice to prebuild a query with aliases and do it like this:
{
_3: items(filter:{id: { _eq: "3" }}){data}
_1: items(filter:{id: { _eq: "1" }}){data}
_2: items(filter:{id: { _eq: "2" }}){data}
}
Which GraphQL client are you using?
If you're using Apollo, and you really don't have access to the schema/resolvers in the server, you can create a local field and resolve it on your own, and so you can manipulate as much as you want.
Reference
https://www.apollographql.com/docs/react/local-state/managing-state-with-field-policies/#defining
Basically, if you're querying a field like:
query {
someQuery(someFilter: {foo: "bar"}) {
items {
data
}
}
}
You can create a local field and write a typePolicy to it. Then you can query something like:
query {
someQuery(someFilter: {foo: "bar"}) {
items {
data
}
parsedItems #client
}
}
Then you can get data from ìtems and resolve parsedItems locally as you want.
From Redux docs:
This [normalized] state structure is much flatter overall. Compared to
the original nested format, this is an improvement in several ways...
From https://github.com/paularmstrong/normalizr
:
Many APIs, public or not, return JSON data that has deeply nested objects. Using data in this kind of structure is often very difficult for JavaScript applications, especially those using Flux or Redux.
Seems like normalized database-ish data structures are better to work with on front end. Then why GraphQL is so popular if it's whole language style is revolved around quickly getting any nested data? Why do people use it then?
This kind of discussion is off-topic on SO ...
it's not only about [normalized] structures ...
graphql client (like apollo) takes care of all data fetching related nuances (error handling, cache, refetching, data conversion, and many more) also but hardly doable with redux.
Different use cases, you can use both:
keep (complex) app state in redux,
handle data fetching in apollo (you can use it for local state, too).
Let's look at why we want to normalize the cache and what kind of work we have to do to get a normalized cache.
For the main page we fetch a list of TODOs and a list of high priority TODOS. Our two endpoints return the following data:
{
all: [{ id: 1, title: "TODO 1" }, { id: 2, title: "TODO 2" }, { id: 2, title: "TODO 2"}],
highPrio: [{ id: 1, title: "TODO 1" }]
}
If we would store the data like this into our cache, we have a difficult time updating a single todo, because we have to update the todo in every array we have in our store or might have in our store in the future.
We can normalize the data and only store references in the array. This way we can easily update a single todo in a single place:
{
queries: {
all: [{ ref: "Todo:1" }, { ref: "Todo:2" }, { ref: "Todo:2" }],
highPrio: [{ ref: "Todo:1" }}]
},
refs: {
"Todo:1": { id: 1, title: "TODO 1" },
"Todo:2": { id: 2, title: "TODO 2" },
"Todo:3": { id: 3, title: "TODO 3" }
}
}
The downside is, that this shape of data is now much harder to use in our list component. We will have to transform the cache a lot, roughtly like so:
function denormalise(cache) {
return {
all: cache.queries.all.map(({ ref }) => cache.ref[ref]),
highPrio: cache.queries.highPrio.map(({ ref }) => cache.ref[ref]),
};
}
Notice how now updating Todo:1 inside of the cache will update all queries that reference the todo automatically, if we run this function inside of the React component (this is often called a selector in Redux).
The magical thing about GraphQL is that it is a strict specification with a type system. This allows GraphQL clients like Apollo to globally identify objects and normalise that cache. At the same time it can also automatically denormalise the cache for you and update objects in the cache automatically after a mutation. This means that most of the time you have to write no caching logic at all. And this should explain why it is so popular: The best code is no code!
const { data, loading, error } = useQuery(gql`
{ all { id title } highPrio { id title }
`);
This code automatically fetches the query on load, normalizes the response and writes it into the cache. Then denormalizes the cache back into the shape of the query using the cache data. Updates to elements in the cache automatically update all subscribed components.
I'm working with GraphQL and having some trouble finding the best way to pipe variables from the query to the result.
I have a schema like so:
type Fragment {
# The id of the fragment
id: String!
# The key of the fragment
key: String!
# The type of component
component_type: String!
# The params used to build the fragment
params: JSON
# Component data
data: JSON
children: [JSON]
items: [JSON]
}
The fragment is meant as a "cms" fragment. I want to pass some query data through to another backend after this resolves.
My query looks like this:
query getFragmentsWithItems($keys: [String!]!
$platform: PlatformType
$version: String
$userInfo: UserInput
$userId: Int
) {
fragmentsWithItems(keys: $keys, platform: $platform, version: $version, userInfo: $userInfo, userId: $userId) {
key
data
children
params
items
}
}
Here's the problem: I have some query data in the data field from the Fragment. That data is not available until that Fragment has resolved. I want to take that data and send it to a different backend. I want to do this with GraphQL, and I was hoping to do something like:
Fragment: () => {
async query(obj, args, context, info, {modles}) => {
const items = await models.getItems(obj.query_string);
}
}
But I need the user_info and user_id that I passed to the original query. Apparently that is only accessible from the info argument which is not meant to be used.
The other path I've taken is to have a manual resolver that does something like so:
const resolveFI = ({ keys, platform, version, userInfo, userId, models }) => {
if (!keys || !keys.length) {
return Promise.resolve(null);
}
return models.release.get({ platform, version }).then(release =>
Promise.all(
keys.map(key =>
models.fragments.get({
key,
platform,
version,
release: release.id
})
)
).then(data => {
const promises = [];
data.rows.forEach(r => {
if (r.data.query_data) {
const d = {
// Can just ignore
filters: r.data.query_data.filters || {},
user_info: userInfo,
user_id: userId
};
promises.push(
new Promise(resolve => {
resolve(
models.itemSearch.get(d).then(i => ({ items: i.items, ...r }))
);
})
);
}
...etc other backends
This works, however a manual promise chain seems to defeat the purpose of using GraphQL.
The last thing I tried was making items a non-scalar type, something like:
type Fragment {
items: ItemSearchResult(user_info: UserInput) etc
But since I can't pipe the actual result from Fragment to the ItemSearchResult that doesn't work.
I realize this is pretty long-winded so I'm open to edits or clarifying.
I'm looking to see if I've missed a better approach or if I should just bag it and have the client apps do the item query after they get the Fragment data back.
It's not that you're not supposed to use info -- it's just a tremendous pain in the butt to use ;) In all seriousness, it's meant to be used for optimization and more advanced use cases, so you shouldn't hesitate to use it if a better solution doesn't present itself. There are libraries out there (like this one) that you can use to parse the object more easily.
That said, there's a couple of ways I imagine you could handle this:
1.) Inside your query resolver(s)
getFragmentsWithItems: async (obj, args, ctx, info) => {
const fragments = await howeverYouDoThat()
const backendCalls = fragments.map(fragment => {
// extract whatever data you need from the fragment
return asyncCallToBackEnd()
})
await backendCalls
return fragments
}
Unfortunately, if you have a lot of different queries returning fragments, you'll end up with redundancy.
2.) Inside the resolver for an existing field (or an additional one) on the Fragment type.
If you go this route, and you need args passed to the query field, you can extract them using the info. Alternatively, you can also mutate the context object inside your query resolver and attach those arguments to it. Then, all resolvers "below" the query resolver (like the resolvers for your Fragment fields) can access those arguments through the context.
3.) Apollo Server lets you define a formatResponse function when configuring its middleware. This essentially provides a hook to do whatever you want with the response before it's returned to the client. You could parse the response inside that function and make the calls to the other backend from there.
Example:
query {
me {
starredPosts {
id
}
}
}
How can the server notice that only the ids are requested, and use the already-fetched user.starredPosts (an array of ids), instead of calling Posts.findOne(id) for each id?
We had the same problem and are in the process of open-sourcing the tools we've built out over the last year and a half internally to address these issues: https://github.com/4Catalyzer/graphql-node-resource/pull/1.
The solution we use is, for object resolvers like that, to resolve them to a "stub" object that contains only the ID, something like:
const childField = {
type: ChildType,
resolve: obj => ({ id: obj.childId }),
};
Then we use DataLoader to fetch the additional fields on the child objects when they're required by using our own default resolver.
We connect to our internal REST API, which supports batching on those requests, so queries that require additional fields get efficiently dispatched and resolved.
However, this does introduce potential for error when writing custom resolvers, as there's no guarantee that obj actually has the relevant fields. We've addressed this by setting up our static types to prevent unchecked access to properties of obj.
You can examine info.fieldNodes[0].selectionSet.selections or use graphql-fields package:
const postsIds = user.starredPosts
const selectionSet = Object.keys(graphqlFields(info))
const onlySelectingId = isEqual(['__typename', 'id'], selectionSet.sort())
if (onlySelectingId) {
return postIds.map(id => ({ id, __typename: 'Post' }))
} else {
return favs.map(id => Post.findOneById(id))
}
This question already has an answer here:
Auto-update of apollo client cache after mutation not affecting existing queries
(1 answer)
Closed 3 years ago.
I have both the getMovies query and addMovie mutation working. When addMovie happens though, I'm wondering how to best update the list of movies in "Edit Movies" and "My Profile" to reflect the changes. I just need a general/high-level overview, or even just the name of a concept if it's simple, on how to make this happen.
My initial thought was just to hold all of the movies in my Redux store. When the mutation finishes, it should return the newly added movie, which I can concatenate to the movies of my store.
After "Add Movie", it would pop back to the "Edit Movies" screen where you should be able to see the newly added movie, then if you go back to "My Profile", it'd be there too.
Is there a better way to do this than holding it all in my own Redux store? Is there any Apollo magic I don't know about that could possibly handle this update for me?
EDIT: I discovered the idea of updateQueries: http://dev.apollodata.com/react/cache-updates.html#updateQueries I think this is what I want (please let me know if this is not the right approach). This seems better than the traditional way of using my own Redux store.
// this represents the 3rd screen in my picture
const AddMovieWithData = compose(
graphql(searchMovies, {
props: ({ mutate }) => ({
search: (query) => mutate({ variables: { query } }),
}),
}),
graphql(addMovie, {
props: ({ mutate }) => ({
addMovie: (user_id, movieId) => mutate({
variables: { user_id, movieId },
updateQueries: {
getMovies: (prev, { mutationResult }) => {
// my mutation returns just the newly added movie
const newMovie = mutationResult.data.addMovie;
return update(prev, {
getMovies: {
$unshift: [newMovie],
},
});
},
},
}),
}),
})
)(AddMovie);
After addMovie mutation, this properly updates the view in "My Profile" because it uses the getMovies query (woah)! I'm then passing these movies as props into "Edit Movies", so how do I update it there as well? Should I just have them both use the getMovies query? Is there a way to pull the new result of getMovies out of the store, so I can reuse it on "Edit Movies" without doing the query again?
EDIT2: Wrapping MyProfile and EditMovies both with getMovies query container seems to work fine. After addMovie, it's updated in both places due to updateQueries on getMovies. It's fast too. I think it's being cached?
It all works, so I guess this just becomes a question of: Was this the best approach?
The answer to the question in the title is
Use updateQueries to "inform` the queries that drive the other views that the data has changed (as you discovered).
This topic gets ongoing discussion in the react-apollo slack channel, and this answer is the consensus that I'm aware of: there's no obvious alternative.
Note that you can update more than one query (that's why the name is plural, and the argument is an object containing keys that match the name of all the queries that need updating).
As you may guess, this "pattern" does mean that you need to be careful in designing and using queries to make life easy and maintainable in designing mutations. More common queires means less chance that you miss one in a mutation updateQueries action.
The Apollo Client only updates the store on update mutations. So when you use create or delete mutations you need to tell Apollo Client how to update. I had expected the store to update automatically but it doesn’t…
I have founded a workaround with resetStore just after doing your mutation.
You reset the store just after doing the mutation. Then when you will need to query, the store is empty, so apollo refetch fresh data.
here is the code:
import { withApollo } from 'react-apollo'
...
deleteCar = async id => {
await this.props.deleteCar({
variables: { where: {
id: id
} },
})
this.props.client.resetStore().then(data=> {
this.props.history.push('/cars')
})
}
...
export default compose(
graphql(POST_QUERY, {
name: 'carQuery',
options: props => ({
fetchPolicy: 'network-only',
variables: {
where: {
id: props.match.params.id,
}
},
}),
}),
graphql(DELETE_MUTATION, {
name: 'deleteCar',
}),
withRouter,
withApollo
)(DetailPage)
The full code is here: https://github.com/alan345/naperg
Ther error before the hack resetStore