Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimistic responses and data invalidation not working as expected #1653

Closed
strblr opened this issue May 19, 2021 · 2 comments
Closed

Optimistic responses and data invalidation not working as expected #1653

strblr opened this issue May 19, 2021 · 2 comments

Comments

@strblr
Copy link

strblr commented May 19, 2021

urql version & exchanges: 2.0.1, graphcache 4.0.0

I'm having two issues with optimistic responses and data invalidation. I don't know if they are related. To set up the stage, imagine having the following very simplified schema :

type Project {
  id: ID!
  title: String!
  contradiction: Contradiction!
}

type Contradiction {
  id: ID!
  label: String!
  project: Project!
}

Query {
  project(id: ID!): Project!
}

Mutation {
  editContradiction(id: ID!, label: String!): Contradiction!
}

A contradiction is part of a project, and can be edited via Mutation.editContradiction. A project can be retrieved via Query.project.
Let's add the following (also very simplified) Urql client :

const client = createClient({
  url: "...",
  requestPolicy: "cache-and-network",
  exchanges: [
    dedupExchange,
    cacheExchange({
      updates: {
        Mutation: {
          editContradiction({ editContradiction }, _, cache, { optimistic }) {
            if (optimistic) return;
            cache.invalidate({
              __typename: "Project",
              id: editContradiction .project.id
            });
          },
        }
      },
      optimistic: {
        editContradiction (vars, cache) {
          const contradiction = cache.readFragment(
            ContradictionFragmentDoc,
            { __typename: "Contradiction", id: vars.id }
          );
          if (!contradiction) return null;
          return {
            ...contradiction,
            label: vars.label
          };
        }
      }
    }),
    fetchExchange
  ]
})

Here are my issues :

  1. When I trigger editContradiction two times in a row, with the second one being triggered before the first had a chance to return from the server, the optimistic update is not applied for that second one. The UI is just frozen after that second trigger until the mutation actually completes.

  2. When invalidating the contradiction's Project in updates as shown in the code snippet above, the second trigger of editContradiction causes my project data (from a useQuery on Query.project) to be set to null for a brief period of time until it's refetched. This seems to happen after the first call of editContradiction returned from the server although I'm not 100% sure. If I wait for each editContradiction to finish and for Query.project to be called again as a result of the invalidation before calling another editContradiction, Query.project is never set to null. I absolutely don't know what's going on here.

My workaround for the first issue is to store the contradiction state in a React state that can be updated immediately, and call editContradiction without an optimistic response as a side effect of those React state updates.

But I don't have a workaround for the second issue, which currently breaks the UI (because Query.user being null not only violates the typing generated by codegen (which only allows for the actual data or undefined), it also triggers the rendering of a big loader that unmounts the whole project UI). This is a problem for all users clicking fast on mutation buttons while having a slow internet.

Thanks in advance for your help.

@strblr strblr added the bug 🐛 Oh no! A bug or unintented behaviour. label May 19, 2021
@kitten kitten removed the bug 🐛 Oh no! A bug or unintented behaviour. label May 19, 2021
@kitten
Copy link
Member

kitten commented May 19, 2021

When you invalidate you delete data from the cache; hence an optimistic mutation can never invalidate data eagerly and expect a cached response for this data.

Furthermore when the UI doesn't reflect the changes you've made then that means it's being forced to wait for the mutation to complete and it being able to send a request to the API because some of your data is missing.

Most recent discussion with info on this is this one: #1645

Edit: Also I spotted another small thing. Invalidating is an explicit call to delete and invalidate data, like is said, and I spotted that you expect project to be temporarily set to null. Two conditions are however preventing you from seeing this:

  • Schema Awareness must be active for partial data to be generated. Invalidating on its own just makes the cache forget about pieces of information, and once it encounters those missing parts it assumes a cache miss.
  • The project field in your example is actually a required field in your schema. This means that it isn't actually nullable, so even with schema awareness it wouldn't be set to null

@kitten kitten closed this as completed May 19, 2021
@strblr
Copy link
Author

strblr commented May 20, 2021

@kitten

I'm not sure I understand all the implication of your explanation.

When you invalidate you delete data from the cache; hence an optimistic mutation can never invalidate data eagerly and expect a cached response for this data.

Actually, the data of my useQuery (on the project query) is not nullified in case of a single invalidation. And that's fortunate because otherwise how could one refetch outdated queries to repopulate the cache after a mutation without having loaders and spinners popping up all over the app ? Right now, if I call cache.invalidate on my Project one time after a mutation, useQuery is refetched but still shows the old Project (please don't change that haha, Apollo did in this unpopular PR and it was one of the many reasons I switched).

My Query.project data is only nullified if the mutation is called two times too fast. I don't know if it's because the second mutation needs the first one to finish, or if the second mutation needs the query refetches triggered by the first one (after cache invalidation in updates) to finish. Is it possible that the Query.project data is forced to null after two consecutive invalidations on the same entity ?

Furthermore when the UI doesn't reflect the changes you've made then that means it's being forced to wait for the mutation to complete and it being able to send a request to the API because some of your data is missing.

I'm trying to imagine what's going on here :

  1. First mutation is called, thus optimistic.editContradiction is called, the contradiction fragment is complete and the UI updates immediately.
  2. The first mutation returns from the server, updates.Mutation.editContradiction is called and invalidates a specific Project.
  3. This invalidation triggers the refetch of a Query.project query.
  4. While this refetch happens, the second mutation is called, followed by optimistic.editContradiction.
  5. While reading the contradiction fragment, the Contradiction.project field is now missing (invalidated in 2.).
  6. So the optimistic result is not applied and waits for the actual mutation to respond. Therefor, I lost the benefit of using optimistic results.

Does it seem like that's what's going on ?
If so, what would be the solution ? Is there a way to repopulate the cache with new query data after a mutation without deleting cached data (simply overwriting it) ?

and I spotted that you expect project to be temporarily set to null

No, I actually expect project to never be null, as specified in the schema.

This means that it isn't actually nullable, so even with schema awareness it wouldn't be set to null

But why is it, then ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants