Hi all
Lets say i have a system with notifications and chat
notification {
user {
id,
name
}
content:String
}
chatmessage {
user {
id
name
}
message:string
}
I first execute a query to get the list of all notifications and then i subscribe to get subsequent updates. Subsequent updates are merged with the original array using updateQuery
Second i have a chat system in which from the beginning i subscribe to receive new chat messages
The problem is the following
If the notifications array contains 100 entities. 95 of them are from the same user and then that same user sends a chat message
Graphcache will update the user in the cache that will result in the notification query to be reloaded thus resulting in the whole notifications component to be rerendered
If the chat system is very busy with many users chatting at the same time then the notifications component will rerender any time a chat arrives which contains a user that also has a notification
I am using react.memo to avoid rerendering each notificatio but the loop on all the notifications will be executed since the notifications array will be refreshed (due to the new chat message)
How should someone handle this kind of situations?
So first of all, that sounds super interesting! 🙌 thanks for bringing such a great use case to us!
The queries itself I suppose aren’t what you’re having issues with? In theory our cache is built to be tremendously fast so you shouldn’t see slow queries from cache.
What I suppose the issue is that you’re concerned about the redenders in general?
So by default we assume that a rerender is necessary whenever an entity is accessed at all. So that’s why you’re seeing an update while that user may not have changed. That’s generally because resolvers could be customised to do some unexpected things.
If you’re not seeing any performance issues, I wouldn’t be worried, but if your component is indeed becoming slow, it may be helpful to use React.memo — as you’ve mentioned — with a deep equal comparison, for instance react-fast-compare to avoid rerendering all notifications too often.
But generally, I’d approach this the same as in any other React app with Redux or other systems 😊
No performance issues at all. I was just wondering what your suggestions are.
I forgot to mention that users have always the same value (id / name never changes) and i was wondering if graphcache could skip the refresh since the new value is shallowEqual with the previous value.
We could introduce shallow checks on entities, yes, but since on that level we would just check the entire query on each entity, it’s the same as a regular deep equality check 😅
So it essentially amounts to the same as you doing the check manually with React.memo.
And since we don’t want to add these checks where they may not be necessary and renders in React being probably a little more expensive than the caching itself, it may be better to simply resort to React’s tools to deal with this 👍
Thanks a lot
FYI i tried to user react-fast-compare but it breaks since it cannot handle objects with null prototype and from what i see graphcache entities are created using Object.create(null)
Thanks for the report 🙏 we’ll look into that then!
It has something to do with refreshing of the cached values.At first the response arrives with a prototype but once the data is refreshed from the cache then it comes back with null prototype
Do you want me to open an issue in graphcache?
Are you using a persisted store? Since it should always be a null prototype object.
@JoviDeCroock no
Whats more weired is that most of the queries work fine (Have prototype) except the following
fragment BalanceUpdateSendActivityContentFragment on BalanceUpdateSendActivityContent {
amounts {
currency
value
}
}
fragment ActivityFragment on Activity {
id
cursor
created
expiry
actor {
id
name
}
content {
... on BalanceUpdateSendActivityContent {
...BalanceUpdateSendActivityContentFragment
}
}
}
query NotificationsQuery($after: String!) {
me {
id
activities(after: $after) {
edges {
node {
...ActivityFragment
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
And here is a snippet of code with output
const [{ fetching, error, data }] = useQuery<
NotificationsQueryQuery,
NotificationsQueryQueryVariables
>({
query: NOTIFICATIONS_QUERY,
variables: {
after
}
});
if (data) console.log("Data prototype is", Object.getPrototypeOf(data));
First render
Data prototype is
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
constructor: ƒ Object()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
hasOwnProperty: ƒ hasOwnProperty()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toString: ƒ toString()
valueOf: ƒ valueOf()
toLocaleString: ƒ toLocaleString()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
Subsequent renders
Data prototype is null
Is the first render when there's no data yet in your query (fetching state) or when there's already data? The latter would be a bug while the former would be expected'ish
First render with data
Data prototype and value is
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
constructor: ƒ Object()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
hasOwnProperty: ƒ hasOwnProperty()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toString: ƒ toString()
valueOf: ƒ valueOf()
toLocaleString: ƒ toLocaleString()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
{me: {…}, __typename: "Query"}
me:
id: "528c8595-6c35-35a7-a9c4-3a90ab14c40a"
activities:
edges: (25) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
pageInfo: {endCursor: "MjAyMC0wMy0yMFQwMTo0MToxMS44NTg3MjUsMzYzMmM0MTYtMjI4OS04ODcxLTc2YjQtMjI0NzhiMmYzOGM4", hasNextPage: true, __typename: "CursorPageInfo"}
__typename: "Activities"
__proto__: Object
__typename: "User"
__proto__: Object
__typename: "Query"
__proto__: Object
Second render with data
Data prototype and value is null
{__typename: "Query", me: {…}}
__typename: "Query"
me:
__typename: "User"
id: "528c8595-6c35-35a7-a9c4-3a90ab14c40a"
activities:
__typename: "Activities"
edges: (25) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
pageInfo: {__typename: "CursorPageInfo", endCursor: "MjAyMC0wMy0yMFQwMTo0MToxMS44NTg3MjUsMzYzMmM0MTYtM
Just to be clear with second render i mean when the query refreshes due to a Chat entity arriving via a subscription with an actor that is already present in the Activities query
That’s indeed expected behaviour. When the data arrives from the server, we just walk over it and make sure all resolvers are applied.
Afterwards for cache results, we use Object.create(null) since the data hasn’t come from the server to avoid “risky writes and reads”
In the future we may simply add some development-only warnings instead that warn about these specific fields.
So objects with null prototype should be expected?
Btw i have been looking at the code and the line that produces objects with null prototypes is
I modified it from
let data = input || makeDict();
to
let data = input || Object.create(Object.prototype);
and everything works fine
If it is expected how should i handle the case in the original question? Especially now that i am not able to use fast compare.
Should i compare manually?
I think I'll just push a patch that finally gets rid of some of the makeDict()s
Some more information.
If i have maskTypeName to true then the objects do come back with a prototype
I assume its because its doing something to hide __typename
Most helpful comment
Thanks a lot