Watcher not triggered after mutation

I came across the following in the Apollo docs:

If you use fragments which contain ID’s then a query which returns or mutates this fragment and returns a new state for this object will automatically be merged into your cache and any query which references that object will be updated. It may therefore be advantageous to plan your schemas so Fragments are reused in List / Detail queries and then the same Fragment is returned as the result of a mutation.

I have a paginated query that fetches results from the server 10 at a time that are then loaded within a UICollectionView. When a cell is clicked upon on, I mark that cell as read using a mutation and my code is roughly as follows:

# Query
query GetItems {
  item {
    ...MarkAsRead_item
  }
}

# Mutation
mutation MarkItemAsRead($itemID: BigInt!) {
  markItemAsRead(anItemIdentifier: $itemID) {
    item {
      ...MarkAsRead_item
    }
  }
}

# Fragment
fragment MarkAsRead_item on Item {
  id
  __typename
  isRead
}

I call my query as:

GQLClient.shared.apollo.watch(query: GetItems)

and my mutation as:

GQLClient.shared.apollo.perform(mutation: MarkItemAsRead(itemID: itemID)

From the text in the Apollo docs above, I’d expect my watcher to be called since the item in the cache would’ve been updated and I’m using the same fragment in the query and mutation, but this isn’t the case – the watcher is not called. Am I understanding something incorrectly?

Hi @knlshh :wave: - have you implemented cacheKeyForObject at all? If not I suspect that is the cause of the problem.

The documentation is ambiguous to state that simply by using fragments with IDs the response objects would be normalized that way. Unless you instruct the cache to normalize using IDs it’ll use the response path which is not the same between queries and mutations.

Thanks @calvincestari!

That worked partially. I’m not sure why it doesn’t work 100% of the time though. I did some debugging and found that for the first page of my paginated results, the watcher is always called back since the Apollo store’s subscribers has the id of the item that changed in my mutation in its dependentKeys. But randomly for other results, the subscribersdependentKeys don’t have the id of the item that changed in the mutation, but I’m not sure how that’s possible.

Any ideas on what I can try?

Nothing obvious is coming to mind @knlshh, did you manage to find anything else in your debugging?

Nothing yet, but I continue to debug.

@calvincestari from your recommendation above, I implemented cacheKeyForObject and I’m facing another issue. Wondering if you happen to have some ideas around it?

Sample code to paginate:

class DataSource {
    private var endCursor: String?
    private var watcher: GraphQLQueryWatcher<Query>!
    var queryCount = 0 // For debugging

    func fetchData() {
        let query = Query(first: 10, after: endCursor)

        watcher = GQLClient.shared.apollo.watch(query: query, cachePolicy: CachePolicy.fetchIgnoringCacheData) { [weak self] result in

            print("Inside watcher = \(query.variables)")

            guard let self = self else { return }
            guard let data = try? result.get().data else { return }

            // Update query count for debugging
            self.queryCount += 1

            self.endCursor = data.endCursor ?? ""

            // Only for debugging, query again
            if self.queryCount < 3 {
                self.fetchData(trigger: .paginatedLoad)
            }
        }
    }

    deinit {
        watcher.cancel()
    }
}

Output for when 3 pages of results are requested:

// With client.cacheKeyForObject = { $0["id"] }

Inside watcher = Optional(["first": Optional(10), "after": nil])

Inside watcher = Optional(["first": Optional(10), "after": Optional("9")])
Inside watcher = Optional(["first": Optional(10), "after": nil])

Inside watcher = Optional(["first": Optional(10), "after": Optional("19")])
Inside watcher = Optional(["first": Optional(10), "after": Optional("9")])
Inside watcher = Optional(["first": Optional(10), "after": nil])

// Without client.cacheKeyForObject = { $0["id"] }

Inside watcher = Optional(["first": Optional(10), "after": nil])
Inside watcher = Optional(["first": Optional(10), "after": Optional("9")])
Inside watcher = Optional(["first": Optional(10), "after": Optional("19")])

From the output above, the number of times the resultHandler of the watcher is called keeps on growing as more pages of results are requested when cacheKeyForObject is set. This in turn severely degrades app performance. Do you know why this happens and how this can be curbed?