Optimistic UI Not Updating - Apollo - apollo

After making a mutation the UI does not update with a newly added item until the page is refreshed. I suspect the problem is in the update section of the mutation but I'm not sure how to troubleshoot further. Any advice is much appreciated.
Query (separate file)
//List.js
export const AllItemsQuery = gql`
query AllItemsQuery {
allItems {
id,
name,
type,
room
}
}
`;
Mutation
import {AllItemsQuery} from './List'
const AddItemWithMutation = graphql(createItemMutation, {
props: ({ownProps, mutate}) => ({
createItem: ({name, type, room}) =>
mutate({
variables: {name, type, room},
optimisticResponse: {
__typename: 'Mutation',
createItem: {
__typename: 'Item',
name,
type,
room
},
},
update: (store, { data: { submitItem } }) => {
// Read the data from the cache for this query.
const data = store.readQuery({ query: AllItemsQuery });
// Add the item from the mutation to the end.
data.allItems.push(submitItem);
// Write the data back to the cache.
store.writeQuery({ query: AllItemsQuery, data });
}
}),
}),
})(AddItem);

Looks promising, one thing that is wrong is the name of the result of the mutation data: { submitItem }. Because in the optimistic Response you declare it as createItem. Did you console.log and how does the mutation look like?
update: (store, {
data: {
submitItem // should be createItem
}
}) => {
// Read the data from our cache for this query.
const data = store.readQuery({
query: AllItemsQuery
});
// Add our comment from the mutation to the end.
data.allItems.push(submitItem); // also here
// Write our data back to the cache.
store.writeQuery({
query: AllItemsQuery,
data
});
}

I'm not entirely sure that the problem is with the optimisticResponse function you have above (that is the right approach), but I would guess that you're using the wrong return value. For example, here is a response that we're using:
optimisticResponse: {
__typename: 'Mutation',
updateThing: {
__typename: 'Thing',
thing: result,
},
},
So if I had to take a wild guess, I would say that you might want to try using the type within your return value:
optimisticResponse: {
__typename: 'Mutation',
createItem: {
__typename: 'Item',
item: { // This was updated
name,
type,
room
}
},
},
As an alternative, you can just refetch. There have been a few times in our codebase where things just don't update the way we want them to and we can't figure out why so we punt and just refetch after the mutation resolves (mutations return a promise!). For example:
this.props.createItem({
... // variables go here
}).then(() => this.props.data.refetch())
The second approach should work every time. It's not exactly optimistic, but it will cause your data to update.

Related

Apollo: Using executor function server side removes operation names

I currently have the following code in a codebase using "#apollo/client": "^3.4.17",
const getFrontEndApiSchema = async (authToken: string, hostname: string) => {
const executor = async ({
document,
variables,
}: Parameters<Parameters<typeof introspectSchema>[0]>[0]) => {
const fetchResult = await crossFetch(`${resolveApiUri(hostname)}/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authentication-Token': authToken,
},
body: JSON.stringify({ query: print(document), variables }),
})
return fetchResult.json()
}
return makeExecutableSchema({
typeDefs: wrapSchema({
schema: buildClientSchema(await unzipSchema()),
executor,
}),
})
}
export const getSchema = async () => {
const frontEndSchema = await getFrontEndApiSchema()
return stitchSchemas({
subschemas: frontEndSchema ? [frontEndSchema, schema] : [schema],
mergeDirectives: true,
})
}
const apolloClient = createApolloClient(
{
schema,
rootValue: { request: req },
},
getAuthenticationToken(req),
false,
)
Which works and fires off requests. However we noticed during a Telemetry exercise (whereby we are trying to track traces through individual operations in DataDog / NewRelic) that a single operation is effectively being split up into it's constituent queries and sent without it's parent operation name.
It's not so clear to me from reading the docs why I would need to this executor function for graphql queries rather than the standard Apollo link chain (similar to what i'm using for the client side apollo client).
So I removed the unneeded executor function to the following.
makeExecutableSchema({
typeDefs: wrapSchema({
schema: buildClientSchema(await unzipSchema()),
}),
})
This worked in so far as the operations where being made and return a result, however ostensibly it was returning results which matched those which would be returned if unauthenticated, (i.e. no authentication token set in the header).
I've checked my error link and have logged context headers and it appears to have the token.
I've also tried swapping the Schemalink for a normal link with no success.
export default function createApolloClient(
schema: SchemaLink.Options,
token: string,
isTest?: boolean,
) {
const link = from([
authLink(token),
serverErrorLink(),
...(__DEV__ ? [logLink(true)] : []),
new SchemaLink(schema),
])
return new ApolloClient({
link,
cache: createCache(),
ssrMode: true,
queryDeduplication: true,
...(!isTest && {
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
query: { fetchPolicy: 'cache-first' },
},
}),
})
}
A typical graphql operation I'm sending
query myOperationName{
user {
id
firstName
}
query2{
id
}
query3{
id
}
}
When I do print(document) in the body of my original executor function I am getting
query2{
id
}
etc
So my question is how server side do I construct the correct Apollo client/ link chain combo such that operations are not stripped of their operation names? And any additional clarity on whether it's necessary to use the SchemaLink at all if my express server is on a different box to the api it talks to would be helpful

How to delete cache record from Apollo GraphQL client with readQuery and writeQuery

The Apollo GraphQL team says that readQuery and writeQuery are good for 95% of the use cases. I am using useMutation and update and want to remove an item from a cache without having to call refetchQueries. My code is as follows:
const [deleteSpeaker] = useMutation(DELETE_SPEAKER, {
update(cache, { data: {deleteSpeaker}}) {
const { speakers} = cache.readQuery({query: GET_SPEAKERS});
cache.writeQuery({
query: GET_SPEAKERS,
data: { speakers: speakers.filter(speaker => speaker.id !== deleteSpeaker.id) }
});
},
});
What gets returned from readQuery leads me to think I should be filtering for speakers.datalist but when I do that, the cache does not update.
What is the correct way to update cache to reflect a removed record from the GET_SPEAKERS query.
export const DELETE_SPEAKER = gql`
mutation DeleteSpeaker($speakerId: Int!) {
deleteSpeaker(speakerId: $speakerId) {
id
first
last
favorite
}
}
`;
and GET_SPEAKERS
export const GET_SPEAKERS = gql`
query {
speakers {
datalist {
id
first
last
favorite
company
}
}
}
`;
reading apollo docs, this should be something lke:
const [deleteSpeaker] = useMutation(DELETE_SPEAKER, {
update(cache, { data: {deleteSpeaker}}) {
cache.modify({
id: cache.identify(deleteSpeaker.id),
fields: {
comments(existingSpeakerRefs, { readField }) {
return existingSpeakerRefs.filter(
speaker => deleteSpeaker.id !== readField('id', speakerRef)
);
},
},
});
},
});

Apollo: Refetch Queries with Delay

Can I use refetchQueries with some delay. Can I set a delay or can I call refetchQueries in a function?
Thanks!
I was unable to find an elegant way to delay refetchQueries. Instead, I just modify the cache directly as mentioned farther down this section of the documentation. https://www.apollographql.com/docs/react/caching/advanced-topics/#updating-after-a-mutation
Here's a snippet from my solution. The reason I loop through sortPermutations is that the query may have cached data for different sets of variables. This solution loops through all of them.
const [createUser] = useMutation<AddUser, AddUserVariables>(ADD_USER, {
update: (cache, { data }) => {
if (!data?.addUser) return
const cachedUsers = cache.readQuery<GetUsers>({
query: GET_USERS,
variables: sortedColumn,
})
sortPermutations.forEach(({ sortBy, sortOrder }) => {
const users = [data.addUser, ...(cachedUsers?.users ?? [])].sort(
sortUsers(sortBy, sortOrder),
)
cache.writeQuery<GetUsers, GetUsersVariables>({
data: { users },
query: GET_USERS,
variables: { sortBy, sortOrder },
})
})
},
})

Nested resolvers with depth greater than 1

The Problem
Looking at this GraphQL query,
query {
asset {
name
interfaces {
created
ip_addresses {
value
network {
name
}
}
}
}
}
How do I define a resolver for just the network field on ip_addresses?
My First Thought
Reading docs the give examples of single nested queries, e.g
const resolverMap = {
Query: {
author(obj, args, context, info) {
return find(authors, { id: args.id });
},
},
Author: {
posts(author) {
return filter(posts, { authorId: author.id });
},
},
};
So I thought - why not just apply this pattern to nested properties?
const resolverMap = {
Query: {
asset,
},
Asset: {
interfaces: {
ip_addresses: {
network: () => console.log('network resolver called'),
},
},
},
};
But this does not work, when I run the query - I do not see the console log.
Further Testing
I wanted to make sure that a resolver will always be called if its on root level of the query return type.
My hypothesis:
Asset: {
properties: () => console.log('properties - will be called'), // This will get called
interfaces: {
created: () => console.log('created - wont be called'),
ip_addresses: {
network_id: () => console.log('network - wont be called'),
},
},
},
And sure enough my console showed
properties - will be called
The confusing part
But somehow apollo is still using default resolvers for created and ip_addresses, as I can see the returned data in playground.
Workaround
I can implement "monolith" resolvers as follows:
Asset: {
interfaces,
},
Where the interfaces resolver does something like this:
export const interfaces = ({ interfaces }) =>
interfaces.map(interfaceObj => ({ ...interfaceObj, ip_addresses: ip_addresses(interfaceObj) }));
export const ip_addresses = ({ ip_addresses }) =>
ip_addresses.map(ipAddressObj => ({
...ipAddressObj,
network: network(null, { id: ipAddressObj.network_id }),
}));
But I feel that this should be handled by default resolvers, as these custom resolvers aren't actually doing anything, but passing data down to another resolver.
The resolver map passed to the ApolloServer constructor is an object where each property is the name of a type in your schema. The value of this property is another object, wherein each property is a field for that type. Each of those properties then maps to a resolver function for that specified field.
You posted a query without posting your actual schema, so we don't know what any of your types are actually named, but assuming the network field is, for example, Network, your resolver map would need to look something like:
const resolver = {
// ... other types like Query, IPAddress, etc. as needed
Network: {
name: () => 'My network name'
}
}
You can, of course, introduce a resolver for any field in the schema. If the field returns an object type, you return a JavaScript Object and can let the default resolver logic handle resolving "deeper" fields:
const resolvers = {
IPAddress: {
network: () => {
return {
name: 'My network name',
}
}
}
}
Or...
const resolvers = {
Interface: {
ip_addresses: () => {
return [
{
value: 'Some value',
network: {
name: 'My network name',
},
},
]
}
}
}
Where you override the default resolver just depends at what point the data returned from your root-level field no longer matches your schema. For a more detailed explanation of the default resolver behavior, see this answer.

Why is my Apollo Client optimistic reply failing?

I'm recording this to document the answer to a problem that took me several hours to solve. Scenario:
I'm using two mutation queries on a single component in React Apollo-Client. This is a component wrapped into a larger component to form a page. Something like this (this is not the actual code, but it should give the idea):
import { compose } from 'react-apollo';
// submitNewUser contains
// postedBy
// createdAt
// content
// submitRepository contains
// otherProp
const thisProps1 = {
name: 'mutation1',
props: ({ ownProps, mutation1 }) => ({
submit: ({ repoFullName, commentContent }) => mutation1({
variables: { repoFullName, commentContent },
optimisticResponse: {
__typename: 'Mutation',
submitNewUser: {
__typename: 'Comment',
postedBy: ownProps.currentUser,
content: commentContent,
},
},
}),
}),
};
const thisProps2 = {
name: 'mutation2',
props: ({ ownProps, mutation2 }) => ({
submit: ({ repoFullName, commentContent }) => mutation2({
variables: { repoFullName, commentContent },
optimisticResponse: {
__typename: 'Mutation',
submitRepository: {
__typename: 'Comment',
otherProp: 'foobar',
},
},
}),
}),
};
const ComponentWithMutations = compose(
graphql(submitNewUser, thisProps1),
graphql(submitRepository, thisProps2)
)(Component);
Whenever the optimistic response fires, only the second result is fed back to into the query-response in the outer component. In other words, the first query gives an 'undefined' response (but no error), while the second returns an object as expect.
Why??
The property "createdAt" is not included in the optimistic reply.
__typename: 'Comment',
postedBy: ownProps.currentUser,
content: commentContent,
Should be:
__typename: 'Comment',
postedBy: ownProps.currentUser,
createdAt: Date(),
content: commentContent,
A missing field in an optimistic reply will silently fail to return anything to any queries that call that data.