Following the Local state management with reactive variables Best Practices post of your blog I ended up creating some reactive variables and implementing the read function on InMemoryCache to retrieve the data. I use Typescript so I have some models for more complex variables.
I will simplify the code and the problem and skip some fields.
The model:
// models/Notification.ts
export interface Notification {
id: string;
optional?: string;
}
the reactive variable is created this way:
// cache.ts
export const notifications = makeVar<Notification[]>([]);
and the cache entry like this:
// index.ts
cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
notifications: { read: () => notifications() },
}
}
}
});
To modify the data I have created a custom hook like this:
import { Notification } from 'Graphql/client/models/Notification';
import { findIndex } from 'lodash';
import { notifications } from 'Graphql/client/cache';
function useNotifications() {
function addNotification(newNotification: Notification) {
notifications([...notifications(), newNotification]);
}
function removeNotification(target: Notification) {
notifications(notifications().filter(act => act.id !== target.id));
}
return {
addNotification,
removeNotification
};
}
export default useNotifications;
The problem starts when I want to modify this variable. If I do not include the optional field, all subscribed queries to this field return data = undefined. Otherwise, I if include the field (and is not set as undefined), everything works as expected.
const { data } = useQuery(AddNotificationQuery);
const { addNotification } = useNotifications();
console.log(data.notifications); // >> []
addNotitification({ id: 'notif1' });
console.log(data.notifications); // >> Error: cannot read notifications of undefined
addNotitification({ id: 'notif2', optional: undefined });
console.log(data.notifications); // >> Error: cannot read notifications of undefined
addNotitification({ id: 'notif2', optional: 'yes' });
console.log(data.notifications); // >> [{ id: 'notif2', optional: 'yes' }]
Intended outcome:
data field from queries to be updated.
Actual outcome:
data field from queries is set to undefined.
Another problem is that there are no logs (no warning, no errors) in the console, making this problem difficult to debug. In the example bellow is easy to understand the problem.
How to reproduce the issue:
https://codesandbox.io/s/flamboyant-heisenberg-v9657?file=/src/App.js
To reproduce the problem just wait 10 seconds. After 5 seconds a notification with the optional value set is added and printed in the screen and after 10 seconds another notification without the optional field is added, making data to be transformed to undefined.
Versions
System:
OS: Linux 4.15 Ubuntu 18.04.4 LTS (Bionic Beaver)
Binaries:
Node: 13.12.0 - /usr/local/bin/node
Yarn: 1.22.0 - ~/kre/admin/admin-ui/node_modules/.bin/yarn
npm: 6.13.7 - ~/kre/admin/admin-ui/node_modules/.bin/npm
Browsers:
Chrome: 84.0.4147.89
Firefox: 78.0.2
npmPackages:
@apollo/client: ^3.1.2 => 3.1.2
apollo: 2.28.3 => 2.28.3
apollo-upload-client: 13.0.0 => 13.0.0
npmGlobalPackages:
apollo-codegen: 0.20.2
apollo: 2.22.0
Fields with optional values in GraphQL are typically achieved through nullability, so you should be able to use a value of null for the optional field if you want the cache not to complain that it's missing. Here's a good article about that: https://hasura.io/blog/graphql-nulls-cheatsheet/
If these notifications are purely client-side, you might be able to achieve what you want by passing fetchPolicy: "cache-only" and returnPartialData: true as options to useQuery, which will allow the cache to return whatever partial data it has, without making any network requests. The trouble with using fetchPolicy: "cache-first" (the default policy) is that the client will make a network request if the cache data seems incomplete, which might not be what you want.
Another technique is to define a custom read function for the Notification.optional field, which can return any default value you like, when there's no existing cache data to return:
new InMemoryCache({
typePolicies: {
Notification: {
fields: {
optional(existing = null) {
return existing;
},
},
},
},
})
However, this requires that your notification objects have __typename: "Notification" in them, which may be more trouble than the other solutions. Still, I would say this is the most AC3-ish answer of the three.
Thank you for the prompt and instructive reply. You are right, so if I want to take the null approach it would be more accurate to type optional fields like this: optional: string | null instead of optional?: string, wouldn't be?
Even when the error field is working (Error: Cannot query field "notifications" on type "Query".), I think there should be some kind of warning in the console (I have no idea if that is tricky or not) if something like this happens, the data to undefined behavior is hard to debug depending on the circumstances.
I'm hitting the same issue as well. @benjamn, wouldn't it be better to show some warning/error in the console in dev mode so it's simple to understand why data from the query is undefined?
same here, we have encountered the same issue and took a while to get it working
Most helpful comment
I'm hitting the same issue as well. @benjamn, wouldn't it be better to show some warning/error in the console in dev mode so it's simple to understand why data from the query is undefined?