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

Updating cache with local resolvers doesn't update the UI #5781

Closed
jthistle opened this issue Jan 11, 2020 · 13 comments
Closed

Updating cache with local resolvers doesn't update the UI #5781

jthistle opened this issue Jan 11, 2020 · 13 comments

Comments

@jthistle
Copy link
Contributor

See a minimal reproduction here, since it isn't easy to explain: https://codesandbox.io/s/fervent-hawking-o2bt5

If you watch the console, you can see that when you click the button, the resolver is running and the new data is being written to the cache. However, the useQuery query in the UI (App.js) is not rerunning, and the data is stale as a result.

Expected result

  • The UI updates when I press the button to show the new value of the cache.

Versions
@apollo/client @ 3.0.0-beta.20

@jthistle jthistle changed the title Updating client state with local resolvers doesn't update the ui Updating cache with local resolvers doesn't update the UI Jan 11, 2020
@benjamn benjamn self-assigned this Jan 12, 2020
@benjamn benjamn added this to the Release 3.0 milestone Jan 12, 2020
benjamn added a commit that referenced this issue Jan 13, 2020
Solves #5781 by keeping data written by cache.writeData from appearing
incomplete just because it does not have __typename fields that were not
explicitly present in the original query.

Since InMemoryCache#transformDocument is responsible for adding these
__typename fields automatically, it makes sense for the StoreReader
belonging to the InMemoryCache to detect and ignore those fields later,
rather than handling this somewhere else, like the QueryManager#transform
method, where transformDocument is called.

@hwillson Another way of looking at this issue is that this bit of code in
the queryListenerForObserver callback is mishandling "incomplete" data, as
we've observed before:

  const resultFromStore: ApolloQueryResult<T> = {
    data: stale ? lastResult && lastResult.data : data,
    ...

The changes in this commit work around the problem by removing a category
of __typename-related reasons why stale might be true here, but I think we
still need to decide what staleness should mean for this logic.
benjamn added a commit that referenced this issue Jan 13, 2020
)

Solves #5781 by keeping data written by cache.writeData from appearing
incomplete just because it does not have __typename fields that were not
explicitly present in the original query.

Since InMemoryCache#transformDocument is responsible for adding these
__typename fields automatically, it makes sense for the StoreReader
belonging to the InMemoryCache to detect and ignore those fields later,
rather than handling this somewhere else, like the QueryManager#transform
method, where transformDocument is called.
@benjamn
Copy link
Member

benjamn commented Jan 13, 2020

@jthistle Ok, a fix for this issue has been published in @apollo/client@3.0.0-beta.22. Please give it a try when you have a chance!

@jthistle
Copy link
Contributor Author

Thank you, I'll try it and let you know.

@jthistle
Copy link
Contributor Author

Ok, my demo is working great now! Thank you.

However, there is still a problem. If you have a look at this sandbox, you can see the map center has the __typename field at first. After an update its lost this field. Why has this disappeared? Isn't cache.writeData meant to just update what exists? If not, how can I just update it (I'm guessing it may involve querying it all first, then editing it, then rewriting it)?

@benjamn
Copy link
Member

benjamn commented Jan 13, 2020

I haven't dug into that sandbox yet, but I would recommend using cache.writeQuery or cache.writeFragment instead of cache.writeData. You might assume that cache.writeData avoids needing to have a query or fragment, but it actually creates a temporary DocumentNode behind the scenes and then calls one of the other methods, so you might as well use the other methods.

@jthistle
Copy link
Contributor Author

OK, I see. I'll try that and get back to you. Thanks again.

@jthistle
Copy link
Contributor Author

jthistle commented Jan 14, 2020

I'm having a really weird problem along the same lines... but I can't reproduce it in a sandbox 😱. Can you see anything wrong with this setup?:

I have my client cache set up with this initial state:

const initialState = {
  mapControls: {
    viewMap: true,
    zoom: 16,
    center: {
      lat: 0,
      lng: 0,
    },
  },
};

And in my resolvers, I resolve a mutation updateCenter(lat: Number!, lng: Number!): Boolean like this:

const resolvers = {
  Mutation: {
    updateCenter: (_root, { lat, lng }, { cache }) => {
      const query = gql`
        query mapCenter {
          mapControls @client {
            center {
              lat
              lng
            }
            viewMap
          }
        }
      `;

      const { mapControls } = cache.readQuery({ query });

      // Some writing to cache occurs here but is irrelevant

      return true;
    },
  }
// ...
}

The cache.readQuery line gives me an error. There is no message attached to this error, but it only happens if the query I use with it requests viewMap. If I request zoom it's fine. I'm at a loss as to what's causing this.

FWIW, I'm calling this mutation using useMutation from another component using this mutation:

mutation SetMapCenter($lat: Number!, $lng: Number!) {
  updateCenter(lat: $lat, lng: $lng) @client
}

I have no idea what's happening :(

@bstro
Copy link

bstro commented Jan 18, 2020

I believe I am running into the same (original) issue, but I am on apollo-boost – is there an easy way to use this fix without "ejecting"? @benjamn

edit: nevermind. I've migrated off boost to the newest beta (.24) and my issue remains, so perhaps it is something else. conceptually it appears very similar to the original post's codesandbox example

@lifeiscontent
Copy link
Contributor

@benjamn any plans to fix issues with Apollo Local state soon? I've been running into several issues with it. I like the idea of being able to do GraphQL on both client and server but it just seems the local state side of apollo has a lot of issues.

@benjamn
Copy link
Member

benjamn commented Jan 29, 2020

@lifeiscontent See #5799? We are hoping to transition away from the AC 2.x local state implementation, though it's going to remain in the codebase for the initial AC3 launch.

@mschipperheyn
Copy link

mschipperheyn commented Feb 21, 2020

This has a label of "Fixed in prerelease". I'm on V3.0.0-beta.36 and I still see this issue.

Mutation and query in the same component. When I mutate state, the query doesn't refresh even after calling refetchQueries

import gql from 'graphql-tag';

export const INVITE_FRAGMENT = gql`
	fragment inviteFragment on LocalInvite {
		id
		cpf
		phone
		email
	}
`;

export const QUERY_INVITES = gql`
	query Local__Invites {
		local__invites @client {
			...inviteFragment
		}
	}
	${INVITE_FRAGMENT}
`;

export const MUTATION_SET_INVITES = gql`
	mutation Local__SetInvites($invites: [LocalInvite!]!) {
		local__setInvites(invites: $invites) @client {
			...inviteFragment
		}
	}
	${INVITE_FRAGMENT}
`;

const typeDefs = gql`
	type LocalInvite {
		id: ID!
		cpf: String!
		phone: String!
		email: String!
	}
	extend type Query {
		local__invites: [LocalInvite!]!
	}
	extend type Mutation {
		local__setInvites(invites: [LocalInvite!]!): [LocalInvite!]!
	}
`;

const resolvers = {
	Query: {
		local__invites: (root, args, { cache, getCacheKey }) => {
			const res = cache.readQuery({ query: QUERY_INVITES });
                         console.log('res1', res);
			return res.local__invites;
		},
	},
	Mutation: {
		local__setInvites: (root, { invites }, { cache, getCacheKey }) => {
			const newInvites = invites.map(i => ({
				...i,
				__typename: 'LocalInvite',
			}));
                         console.log('write1', newInvites);
			cache.writeQuery({
				query: QUERY_INVITES,
				data: {
					local__invites: newInvites,
				},
			});
			return newInvites;
		},
	},
};

export default {
	typeDefs,
	resolvers,
};

const MyComponent = () => {
	const {
        data: { local__invites },
        loading, 
        error,
	} = useQuery(QUERY_INVITES);
	const [saveInvites, dataInvites] = useMutation(MUTATION_SET_INVITES);
	const [activeDependent, setActiveDependent] = React.useState(null);

	React.useEffect(() => {
		saveInvites({
			variables: {
				invites: [
					{
						id: 123,
						cpf: '213123',
						email: 'marc@masda.nl',
						phone: '1231',
					},
				],
            },
            refetchQueries: () => [
				{
					query: QUERY_INVITES,
				},
			],
        });
        
    }, []);
    console.log(
		'selected1',
		local__invites,
		loading,
		error,
		dataInvites?.data,
		dataInvites?.loading,
		dataInvites?.error,
	); 

	return null;
};

export default OwnerInviteDependents;

The output sequence is:

selected1 Array [] true undefined undefined false undefined
selected1 Array [] true undefined undefined true undefined
res1 Object {
  "local__invites": Array [],
}
write1 Array [
  Object {
    "__typename": "LocalInvite",
    "cpf": "213123",
    "email": "marc@masda.nl",
    "id": 123,
    "phone": "1231",
  },
]
selected1 Array [] false undefined undefined true undefined
res1 Object {
  "local__invites": Array [],
}
selected1 Array [] false undefined Object {
  "local__setInvites": Array [
    Object {
      "__typename": "LocalInvite",
      "cpf": "213123",
      "email": "marc@masda.nl",
      "id": 123,
      "phone": "1231",
    },
  ],
} false undefined

As I step through the code, I can see that immediately after writing the data, it's in the cache ('write1'). But the subsequent query ('res1') shows a cache that is empty. So, it seems like the cache reference is outdated or is reset.

We get an error if we use react-native-debugger similar to the one we got before we implemented the react-native-debugger network inspect fix. The same error doesn't occur with standard Chrome debugger.

Uncaught TypeError: Cannot read property 'get' of undefined
    at /Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/react-devtools-core/build/standalone.js:37
    at Array.forEach (<anonymous>)
    at t.value (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/react-devtools-core/build/standalone.js:37)
    at /Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/react-devtools-core/build/standalone.js:37
    at /Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/react-devtools-core/build/standalone.js:37
    at Array.forEach (<anonymous>)
    at /Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/react-devtools-core/build/standalone.js:37
    at Array.forEach (<anonymous>)
    at e.value (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/react-devtools-core/build/standalone.js:37)
    at /Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/react-devtools-core/build/standalone.js:3
    at Array.forEach (<anonymous>)
    at WebSocket.e.onmessage (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/react-devtools-core/build/standalone.js:3)
    at WebSocket.onMessage (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/EventTarget.js:99)
    at WebSocket.emit (events.js:203)
    at Receiver._receiver.onmessage (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/WebSocket.js:141)
    at Receiver.dataMessage (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/Receiver.js:389)
2app.html:1 Uncaught SyntaxError: Unexpected token � in JSON at position 0
    at JSON.parse (<anonymous>)
    at WebSocket.e.onmessage (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/react-devtools-core/build/standalone.js:3)
    at WebSocket.onMessage (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/EventTarget.js:99)
    at WebSocket.emit (events.js:203)
    at Receiver._receiver.onmessage (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/WebSocket.js:141)
    at Receiver.dataMessage (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/Receiver.js:389)
    at Receiver.getData (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/Receiver.js:330)
    at Receiver.startLoop (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/Receiver.js:165)
    at Receiver.add (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/Receiver.js:139)
    at Socket.<anonymous> (/Applications/React Native Debugger.app/Contents/Resources/app.asar/node_modules/ws/lib/WebSocket.js:138)
    at Socket.emit (events.js:203)
    at addChunk (_stream_readable.js:295)
    at readableAddChunk (_stream_readable.js:276)
    at Socket.Readable.push (_stream_readable.js:210)
    at TCP.onStreamRead (internal/stream_base_commons.js:166)

@mschipperheyn
Copy link

If instead of cache.writeQuery and cache.readQuery, I use client.writeQuery and client.readQuery I get the following slightly better, but still problematic output result

selected1 Array [] true undefined undefined false undefined
selected1 Array [] true undefined undefined true undefined
res1 Object {
  "local__invites": Array [],
}
write1 Array [
  Object {
    "__typename": "LocalInvite",
    "cpf": "213123",
    "email": "marc@masda.nl",
    "id": 123,
    "phone": "1231",
  },
]
selected1 Array [
  Object {
    "__typename": "LocalInvite",
    "cpf": "213123",
    "email": "marc@masda.nl",
    "id": 123,
    "phone": "1231",
  },
] true undefined undefined true undefined
selected1 Array [] false undefined undefined true undefined
res1 Object {
  "local__invites": Array [],
}
selected1 Array [] false undefined Object {
  "local__setInvites": Array [
    Object {
      "__typename": "LocalInvite",
      "cpf": "213123",
      "email": "marc@masda.nl",
      "id": 123,
      "phone": "1231",
    },
  ],
} false undefined

Now the query gets an update, but the cache apparently remains empty or gets reset.

@mschipperheyn
Copy link

mschipperheyn commented Feb 26, 2020

Just a follow up on my comments here. I create a code repo and I couldn't reproduce the issue there. So obviously, something else is going on. For those who might find it useful: https://codesandbox.io/s/naughty-glitter-wzmof The actual cause for my issue was an undetected remount of the ApolloProvider which lead to the cache being cleaned.

@benjamn benjamn closed this as completed May 28, 2020
@chrisgwgreen
Copy link

I've spent the past part of a day trying to understand why my cache was updating via the client.writeFragment but the update not being reflected in the UI/broadcast. Ultimately it was because a query was being triggered which had fetchPolicy: 'network-only' set. Just wanted to put it out there.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants