简体   繁体   中英

Apollo GraphQL Optimistic Response and updating the store issue

I'm running into a bit of a problem when updating the UI after a mutation using an Optimistic Response and altering the apollo cache. I have the following Mutation:

mutation updateCart($basketId: ID!, $productId: ID!, $amount: Int!) {
  setOrderItem(
    basketId: $basketId,
    productId: $productId,
    amount: $amount
  ) {
    id
    amount
    product
  }
}

And the following Query:

query cart($userId: ID!) {
  User(id: $userId) {
    id
    basket {
      id
      items {
        id
        amount
        product {
          id
          name
          price
          imageUrl
          description
        }
      }
    }
  }
}

In my React component I have a button which fires the following method:

addToCart = async (product) => {
  const basketId = this.props.basketId
  const items = this.props.userCartQuery.User.basket.items
  let amount = 1

  for (const item of items) {
    if(item.product.id === product.id) {
      amount = item.amount + 1
    }
  }

  await this.props.updateCartMutation({
    variables: {
      basketId: basketId,
      productId: product.id,
      amount: amount
    },
    optimisticResponse: {
      __typename: 'Mutation',
      setOrderItem: {
        id: Math.round(Math.random() * -1000000),
        amount: amount,
        product: product,
        __typename: 'SetOrderItemPayload'
      }
    },
    update: (store, { data: { setOrderItem } }) => {
      try {
        const data = store.readQuery({
          query: USER_CART_QUERY,
          variables: {
            userId: this.props.userId,
          }
        })
        let replace = false
        for (const key in items) {
          if (items[key].product.id === product.id) {
            data.User.basket.items.splice(key, 1, setOrderItem)
            replace = true
          }
        }
        if (!replace) {
          data.User.basket.items.push(setOrderItem)
        }
        store.writeQuery({ query: USER_CART_QUERY, data })
      }
      catch(err) {
        console.log(err)
      }
    }
  })
}

Here's what I expect. addToCart will by default fire the updateCartMutation with an amount = 1 unless the product.id already exists in items, in which case it will increment amount by 1. The optimistic response is mocked out and the update should read the USER_CART_QUERY from the store. If the item already exists in the query we splice out the original item from the store and replace it with the setOrderItem payload. If the item doesn't exist, we push the payload to items. Finally we write the query to the store.

When I'm adding a new item to the cart, everything works perfectly. I notice that the optimistic response is mocked and the cart query is read from the store by update. I successfully push the payload into items and immediately my UI is updated. Shortly after that update is called again with the response from the server. This is pushed into items, written to the store, and everything works as expected.

The first time that I try to add an item which already exists in items, it seems to work fine. Also, if I attempt after a fresh refresh, everything works fine. The second time I try to add, I get an error. What I'm noticing is that the optimistic response is mocked out, spliced into items and then written to the store, but when update is called again with the update from the server, I get the following error:

"Encountered a sub-selection on the query, but the store doesn't have an object reference. This should never happen during normal use unless you have custom code that is directly manipulating the store; please file an issue."

The error is thrown in readFromtStore.js:

  export function assertIdValue(idValue) {
    if (!isIdValue(idValue)) {
        throw new Error("Encountered a sub-selection on the query, but the store doesn't have an object reference. This should never happen during normal use unless you have custom code that is directly manipulating the store; please file an issue.");
    }
  }

At this point, idValue is a json object representing the Product from the payload.

Here is setOrderItem.graphql

type SetOrderItemPayload {
  id: ID
  amount: Int!
  product: Json
}

extend type Mutation {
  setOrderItem(
    basketId: ID!
    productId: ID!
    amount: Int!
  ): SetOrderItemPayload!
}

And in setOrderItem.ts I'm returning product from

product: Product(id: $productId) {
  id
  name
  price
  description
  imageUrl
  __typename
}

The last thing I want to point out is that if I remove product from the return in the UPDATE_CART_MUTATION, I can add products that already exist with no issues. The problem then is that if I try to add a product that doesn't already exist in items, when a separate component renders, the User isn't returned from the query and I get an undefined error related to a count of the items in the cart.

This question is huge now and I may be down a hole but hopefully someone will have some idea how to help.

So I figured out a solution, but I'm still hoping for a better explanation. In my mutation, I added a directive on returning product conditionally and passed in a boolean variable whether I wanted the product data to return with the mutation (Which I need when adding a unique product, but don't want when updating the amount of a product which exists in the items array.

The mutation looks like:

mutation updateCart($basketId: ID!, $productId: ID!, $amount: Int!, $updateCart: Boolean!) {
    setOrderItem(
      basketId: $basketId,
      productId: $productId,
      amount: $amount
    ) {
      id
      amount
      product @skip(if: $updateCart)
    }
  }

and my addToCart function:

addToCart = async (product) => {
    const basketId = this.props.basketId
    const items = this.props.userCartQuery.User.basket.items
    let amount = 1
    let updateCart = false

    for (const item of items) {
      if(item.product.id === product.id) {
        amount = item.amount + 1
        updateCart = true
      }
    }

    await this.props.updateCartMutation({
      variables: {
        basketId: basketId,
        productId: product.id,
        amount: amount,
        updateCart: updateCart
      },
      optimisticResponse: {
        ...Rest of function...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM