I have a query that returns multiple nested objects to render a screen full of information. I want to delete one of the deeply-nested objects and update the screen optimistically (i.e. without running the complete query).
To explain the query and UI, I'll use a Trello board -like query as an example:
query everything {
getBoard(id: "foo") {
name
cardLists {
edges {
node {
id
name
cards {
edges {
node {
id
name
}
}
}
}
}
}
}
}
The result of this query is used to build a UI like this: https://d3uepj124s5rcx.cloudfront.net/items/170a0V1j0u0S2p1l290I/Board.png
I'm building the app using:
VueJS
Vue Apollo
Scaphold.io as my GraphQL store
When I want to delete one of the cards, I call:
deleteCard: function () {
this.$apollo.mutate({
mutation: gql`
mutation deleteCard($card: DeleteCardInput!) {
deleteCard(input: $card) {
changedCard {
id
}
}
}
`,
variables: {
'card': {
id: this.id
}
},
})
.then(data => {
// Success
})
}
The mutation is successfully deleting the card, but I want to update the UI immediately based on the mutation. The simple option for doing this is to call refetchQueries: ['everything'] — but this is an expensive query and too slow for quick UI updates.
What I want to do is update the UI optimistically, but the example mutations for Vue Apollo don't address either deletes or the deeply-nested scenario.
What is the right solution / best-practices for deleting an item from a deeply-nested query, and optimistically updating the UI?
If you look at the documentation for the optimistic Response of apollo you see that the optimistic response "fakes" the mutation result.
So it will handle the updateQueries function with the optimistic values. In your case this means that if you add the optimisticResponse property it should alter the query inside the apollo store with the optimistic values:
Could look like this:
this.$apollo.mutate({
mutation: gql `
mutation deleteCard($card: DeleteCardInput!) {
deleteCard(input: $card) {
changedCard {
id
}
}
}
`,
variables: {
'card': {
id: this.id
}
},
updateQueries: {
// .... update Board
},
optimisticResponse: {
__typename: 'Mutation',
deleteCard: {
__typename: 'Card', // graphQL type of the card
id: this.id,
},
},
})
Related
I am trying to dynamically fetch & filter data from API to a component in Vue using Apollo and GraplhQL. However, when the category value (this.category) is updated via user action (#click event), it causes the entire page to refresh.
Component code:
apollo: {
cards: {
prefetch: true,
query: cardsQuery,
variables() {
return {
category: this.category,
}
}
}
},
methods: {
categorySelector(category) {
this.category = category;
},
},
gql query:
query cards ($section: String){
cards (
where: { category: {section: $section} }
) {
name
category {
section
}
}
}
Am I doing something wrong with my graphql / apollo query or is the issue somewhere else?
After days crawling through documentation I am unable to get past this issue.
I am building a currency toggle which will allow customers to switch between $AUD and $USD currencies in the UI. I am attempting to implement this via Shopify's Storefront API International Pricing GraphQL queries, specifically the #inContext directive.
I'm storing a countryCode variable in the Vuex store (which is updated via a toggle in the UI) and passing it into an Apollo Smart Query as a reactive variable in the page component.
I am able to successfully retrieve product data/prices for a certain country on first load but my issue is when updating the countryCode Smart Query variable via a Vuex action it has no effect on the Apollo data even though the Vuex data updates as desired.
I'm not sure if this issue is directly related to the Shopify Storefront API or a mistake with my Apollo query?
FYI – when updating the reactive variable productHandle, the data automatically updates as expected.
Page template
export default {
data: () => ({
productHandle: "product-name",
}),
// Get selected country code from Vuex store module
computed: {
currentCountryCode() {
return this.$store.getters["currency/countryCode"];
},
},
// Pass country code to Smart query variable
apollo: {
product: {
client: "shopify",
query: ProductByHandle,
variables: function () {
return {
handle: this.productHandle,
countryCode: this.currentCountryCode, // this should be reactive but doesn't effect query when variable is updated/changes
};
},
update(data) {
return _get(data, "productByHandle", {});
},
deep: true, // has no noticeable effect
},
},
}
GraphQL query (for context)
query ProductByHandle($handle: String!, $countryCode: CountryCode!) #inContext(country: $countryCode) {
productByHandle(handle: $handle) {
id
title
handle
productType
tags
description
descriptionHtml
availableForSale
vendor
images(first: 10) {
edges {
node {
transformedSrc
}
}
}
priceRange {
minVariantPrice{
amount
currencyCode
}
}
}
}
Any guidance or help would be greatly appreciated!
I've been finding that I have to set fetchPolicy: 'no-cache' on the Apollo query if I'm passing the product data to a component. If you're using the product data where the Apollo Query happens (i.e. same component / page ), you can skip the fetchPolicy and just add the watcher.
apollo: {
product: {
client: "shopify",
query: ProductByHandle,
fetchPolicy: 'no-cache',
variables: function () {
return {
handle: this.productHandle,
countryCode: this.currentCountryCode
}
}
}
}
Next, I've been using a watcher and call a method to update the pricing. For example:
methods: {
updatePrice(product) {
this.minPrice = Number(product.node.priceRange.minVariantPrice.amount).toFixed(2),
this.maxPrice = Number(product.node.priceRange.maxVariantPrice.amount).toFixed(2),
this.compareAtMinPrice = Number(product.node.compareAtPriceRange.minVariantPrice.amount).toFixed(2),
this.compareAtMaxPrice = Number(product.node.compareAtPriceRange.maxVariantPrice.amount).toFixed(2)
}
},
watch: {
product: function(newval, oldval) {
this.updatePrice(newval)
}
}
This works, but I'd love to figure out a solution that includes having caching. I suspect using a mutation when the currency changes might do it, but haven't dug into it.
I have a form where the user edits the object - in my case, metadata about a story - after it is loaded from GraphQL. However, when I use my vue-router guard to check if the story has been changed, the story state is always the modified value.
Vuex story.js
...
getters: {
...formSubmit.getters,
getStory: (state) => {
return state.story},
getEditedStory: (state) => state.editedStory,
getStoryDescription: (state) => {
return state.story.description
}
},
mutations: {
...formSubmit.mutations,
setStory(state, payload) {
state.story = payload
},
setEditedStory(state, payload) {
state.editedStory = payload
}
},
...
Form component
export default {
...
apollo: {
story: {
query: gql`query GetStory($id: ID!) {
story(id: $id) {
name
summary
description
}
}`,
variables() {
return {
id: this.id
}
},
result({ data}) {
this.setText(data.story.description)
this.setStory(data.story)
this.setEditedStory(data.story)
},
}
},
...
In my form I have the values mapped with v-model:
<v-text-field
v-model="story.name"
class="mx-4"
label="Add your title..."
single-line
:counter="TITLE_TEXT_MAX_LENGTH"
outlined
solo
:disabled="updateOrLoadInProgress"
/>
However, for some reason whenever I call this.getStory its value is modified accordingly to the v-model. Why?
Although I still don't quite understand why, it seems like the changes to the apollo variable story affect the store.story values with using the mutations to set them.
I've modified the initial setters to be like this:
this.setText(data.story.description)
let loadedStory = {
name: data.story.name,
description: data.story.description,
summary: data.story.summary,
}
this.setStory(loadedStory)
this.setEditedStory(data.story)
},
which seems to prevent the state.story from following the changes to the apollo created story variable. This seems like unexpected behaviour, and I don't quite understand what part of javascript object assignment makes this work this way.
Imagine the following routes. I'm using Vue and vue-router syntax right now, but I figure the question applies to other SPA frameworks as well.
{
path: 'user/:id', component: require('User.vue'),
children: [
{ path: 'edit', component: require('UserEdit.vue'), }
]
}
In User.vue, the user object is fetched using the route id parameter upon component creation:
data() {
return { user: null }
},
created() {
this.user = fetchUser(this.$route.params.id)
}
In UserEdit.vue, a user is also fetched, and in 85% of the cases this will be the user that was also fetched in User.vue:
data() {
return { user: null }
},
created() {
this.user = fetchUser(this.$route.params.id)
}
Question: if we would navigate from User.vue to UserEdit.vue, it is apparent that (most probably) the same user object will be fetched again. How can this kind of code duplication be avoided? How should I pass the previously fetched data down to a child route?
I guess I should somewhere check if the route parameters remain equal, because if they aren't we're editing another user and the User data should be fetched anyway...
Time for a state management store (like vuex)? If so, when the app navigates away from user pages, should the user store be cleared, or do you keep the last fetched user always in memory?
I'm having a hard time to come up with something DRY.
Looking forward to your advice and some hands-on code examples.
Use vuex for state management. For example, setting something like lastUser and userData which could be accessed from any component. fetchUser would then be an action in the store:
Store
state: {
lastUser: '',
userData: null
},
actions: {
fetchUser({ state }, user) {
if (state.userData && user == state.lastUser) {
return state.userData;
} else {
// Api call, set userData and lastUser, return userData
}
}
}
User
async created() {
this.user = await this.$store.dispatch('fetchUser', this.$route.params.id);
}
UserEdit
async created() {
this.user = await this.$store.dispatch('fetchUser', this.$route.params.id);
}
I have a higher order component in my react native application that retrieves a Profile. When I call an "add follower" mutation, I want it to update the Profile to reflect the new follower in it's followers collection. How do I trigger the update to the store manually. I could refetch the entire profile object but would prefer to just do the insertion client-side without a network refetch. Currently, when I trigger the mutation, the Profile doesn't reflect the change in the screen.
It looks like I should be using the update option but it doesn't seem to work for me with my named mutations. http://dev.apollodata.com/react/api-mutations.html#graphql-mutation-options-update
const getUserQuery = gql`
query getUserQuery($userId:ID!) {
User(id:$userId) {
id
username
headline
photo
followers {
id
username
thumbnail
}
}
}
`;
...
const followUserMutation = gql`
mutation followUser($followingUserId: ID!, $followersUserId: ID!) {
addToUserFollowing(followingUserId: $followingUserId, followersUserId: $followersUserId) {
followersUser {
id
username
thumbnail
}
}
}`;
...
#graphql(getUserQuery)
#graphql(followUserMutation, { name: 'follow' })
#graphql(unfollowUserMutation, { name: 'unfollow' })
export default class MyProfileScreen extends Component Profile
...
this.props.follow({
variables,
update: (store, { data: { followersUser } }) => {
//this update never seems to get called
console.log('this never triggers here');
const newData = store.readQuery({ getUserQuery });
newData.followers.push(followersUser);
store.writeQuery({ getUserQuery, newData });
},
});
EDIT: Just realised that you need to add the update to the graphql definition of the mutation.
EDIT 2: #MonkeyBonkey found out that you have to add the variables in the read query function
#graphql(getUserQuery)
#graphql(followUserMutation, {
name: 'follow',
options: {
update: (store, { data: { followersUser } }) => {
console.log('this never triggers here');
const newData = store.readQuery({query:getUserQuery, variables});
newData.followers.push(followersUser);
store.writeQuery({ getUserQuery, newData });
}
},
});
#graphql(unfollowUserMutation, {
name: 'unfollow'
})
export default class MyProfileScreen extends Component Profile
...
this.props.follow({
variables: { .... },
);
I suggest you update the store using the updateQueries functionality link.
See for example this post
You could use compose to add the mutation to the component. Inside the mutation you do not need to call client.mutate. You just call the mutation on the follow user click.
It might also be possible to let apollo handle the update for you. If you change the mutation response a little bit that you add the following users to the followed user and add the dataIdFromObject functionality. link