In a graphql_flutter book-club app, why does a newly posted review not appear in the list after the Mutation succeeds, and how do you fix it?
answer
- edits merge, additions do not
- return id and __typename
- MutationOptions.update with the cache
- readQuery then writeQuery
- optimisticResult on runMutation
basics
~20 sThe cache merges changed fields of entities it already holds, but it cannot know a new review belongs in a cached list. Add it in MutationOptions.update with cache.readQuery and cache.writeQuery, or refetch the list query.
solid answer
~40 s`GraphQLCache` normalises entities by `__typename` and `id`, which `gql()` adds to selections automatically, so a mutation that returns an edited review with its id updates every query showing that review. A new review is different: no cached list contains it, and the cache cannot infer which list field it belongs to. So in `MutationOptions.update`, which receives the cache proxy and the result, I read the book's reviews query with `cache.readQuery(request)`, append the new review, and `cache.writeQuery(request, data: ...)`; watching `Query` widgets rebroadcast. The simpler alternative is calling the list's `refetch`. Passing `optimisticResult` to `runMutation` shows the review immediately, because `update` runs once with the optimistic data and again with the server result.
code
dart · 28 linesfinal addReview = gql(r'''
mutation AddReview($bookId: ID!, $text: String!) {
addReview(bookId: $bookId, text: $text) { id text author { id name } }
}
''');
MutationOptions addReviewOptions(String bookId) => MutationOptions(
document: addReview,
update: (GraphQLDataProxy cache, QueryResult? result) {
final created = result?.data?['addReview'];
if (result == null || result.hasException || created == null) return;
final request = Operation(document: bookReviews)
.asRequest(variables: {'bookId': bookId});
final current = cache.readQuery(request);
if (current == null) return;
final reviews = [
created,
...(current['book']['reviews'] as List<dynamic>),
];
cache.writeQuery(
request,
data: {
...current,
'book': {...current['book'] as Map<String, dynamic>, 'reviews': reviews},
},
);
},
);go deeper
Recall that a mutation's result does not add itself to cached lists, so you refetch the list or update the cache yourself.
Explain the update callback with readQuery and writeQuery, why edited entities propagate automatically, and how refetch compares.
Choose between cache writes and refetches per list, handle variables as part of the cache key, and design optimistic updates that roll back cleanly.
Weigh client-side cache maintenance against server-driven invalidation or subscriptions, considering how many screens and teams share one cache.
## The symptom A book page shows reviews through a `Query` widget. A member writes a review, the `Mutation` completes without error, and the list still shows the old reviews until the page is reopened or refreshed. Editing an existing review's text, by contrast, updates everywhere at once. Both behaviours come from how `graphql_flutter`'s cache stores data. ## Why edits propagate and additions do not `GraphQLCache` is **normalised**: each object with a `__typename` and an `id` is stored once, under a key such as `Review:42`, and query results hold references to those keys. Two practical consequences: - the **`gql()` helper adds `__typename`** to every selection set when it parses a document, so you only have to select `id`; - when a mutation's response contains `Review:42` with new fields, the cache updates that record, and **every watched query that references it rebroadcasts**. A new review is a new record that no cached result references. The book's `reviews` list in the cache is a list of references, and nothing tells the client that the mutation's result should be appended to it, or to which of several cached lists (per book, per member, per sort order). The general theory of normalised caches is covered with GraphQL itself; the Flutter task is to tell this cache what changed. ## Fix 1: update the cache in MutationOptions.update `MutationOptions` takes an **`update`** callback, `(GraphQLDataProxy cache, QueryResult? result)`. Inside it: 1. Build the **same request** the list query uses: `Operation(document: reviewsQuery).asRequest(variables: {'bookId': bookId})`. 2. **Read** the current data with `cache.readQuery(request)`; it may be `null` if the list was never loaded. 3. **Append** the new review from `result.data` to the list. 4. **Write** it back with `cache.writeQuery(request, data: updated)`. Watching queries then rebroadcast the new list. The callback also receives `result.hasException`, so a failed mutation can leave the cache alone. ## Fix 2: refetch The `Query` builder receives a **`refetch`** function; calling it from the mutation's `onCompleted` reloads the list from the server. It costs a round trip but cannot get the list shape wrong, which makes it a reasonable default for lists with server-side sorting or filtering. ## Optimistic results `runMutation(variables, optimisticResult: {...})` lets the UI update before the server answers: - `update` runs **twice**: first with the optimistic data, then with the real result; - the optimistic write is layered on top of the cache and **discarded** when the real result arrives; - the cache's default `partialDataPolicy`, `acceptForOptimisticData`, is lenient with optimistic payloads that miss fields such as `__typename`, while real data is checked strictly. ## Checklist | Situation | Approach | |---|---| | an existing entity's fields changed | return `id` and the changed fields; nothing else needed | | a new item for one known list | `update` with `readQuery`/`writeQuery` | | many lists, server-side ordering | `refetch` the affected queries | | item deleted | `update` removing it from lists | | instant feedback | `optimisticResult` plus `update` | ## Testing a cache update Cache callbacks are ordinary Dart functions over a `GraphQLDataProxy`, so they can be tested without widgets: 1. Create a `GraphQLCache()` (in-memory) and write a known reviews list with `writeQuery`. 2. Call your `update` function with that cache and a `QueryResult` built from a recorded mutation response. 3. `readQuery` the list again and assert the new review is present, in the expected position. 4. Repeat with a result that has an exception, and assert the list is unchanged. This catches the most common failure, a mutation selecting fewer fields than the list query, before it reaches users as a list that silently fails to update. ## Pitfalls - Writing data that does not match the query's shape is rejected by the strict write checks, so select the same fields in the mutation that the list query selects. - A `noCache` list query never enters the cache, so no cache update can reach it; use `refetch` there. - Variables are part of the cache key: updating the list for `sort: NEWEST` leaves the `sort: TOP` list unchanged.
- Why does editing a review's text update every screen without any update callback?The cache stores the review once under its `__typename` and `id`, and `gql()` adds `__typename` to selections for you. When the mutation returns that review with the new text, the shared record changes and every watched query referencing it is rebroadcast.
- When is refetch a better choice than writing to the cache?When the new item's position depends on server logic, such as sorting, filtering or pagination, or when several cached lists might contain it. Refetching costs a request but always matches the server, while hand-written cache updates must reproduce the server's rules.
- What happens to an optimistic result if the mutation fails?The optimistic layer is discarded when the real result arrives, so the UI rolls back to the cached data. `update` runs again with the failed result, where checking `result.hasException` lets you skip the real write.
saying these in an interview costs you the question
- The cache automatically appends new items to every list of that type
- You must select __typename by hand in every query for caching to work
- A successful mutation always refetches all active queries
- Optimistic results are written permanently to the cache
- update runs only once, after the server responds