Comments (8)
I fear I'm still missing a bit of context here. Can you maybe take a few steps back and start describing what you want to do in the end?
That said:
- please don't use
useEffect
with a subscription result - you probably want to use theonData
callback - please don't use
useEffect
to trigger a lazyQuery.useLazyQuery
should respond to user interaction, not to React renders. You probably want to useuseQuery
with a skip option instead.
from apollo-client.
Good morning and thank you @phryneas .
- We have a parent entity -
Conversation
that has a connection to edges of typeStep
. - We have a page listing
Steps
inside aConversation
. It has some logic tofetchMore
by shifting over thePageInfo
cursor. It's really just a query that goes something likequery Steps(...) { node(id: ...) { ... on Conversation { steps(last: ..., before: ... { ... } ) } } }
- We have a subscription that triggers when a new
Step
becomes part of aConversation
remotelly, maybe created by another user. - Upon this subscription firing, we receive the
Step
that was added, and we want to add it to the Apollo cache - the one being used behind the scenes, being primed and filled by the page listing theStep
entities.
So from what we understood, a good way to do so would be to use cache.modify
on the Connection
edges to append the Step
received by the subscription to the edges that were already fetched.
What I don't understand is how to do this in a "type safety" fashion, or even how to approach the data structures that should be involved in pushing new elements to the cache. It'd be nice to have a working example of such scenario.
In response to your comments:
- We don't use
useLazyQuery
lightly inuseEffect
. Sometimes it's necessary - and with the right dependencies we never had problems with that. I'm curious why you flag this though, would you care to ellaborate please? In our case, usinguseQuery
along with theskip
flag isn't an option, because we can't have thisuseQuery
declared upfront with the variables - as our component is mounted with these variables being optional - and therefore can't compile without them being set to a value. This is what we want from the strong typing guarantees at compile-time - and instead we want the query to only fire when the variables become set. We also don't want to start getting into "nested nested components" hell so that we can have a conditionally rendered component thatuseQuery
internally, it essentially results in the same internal logic with more complexity in our code. - Noted, very good to know. Thanks!
from apollo-client.
This is what we want from the strong typing guarantees at compile-time - and instead we want the query to only fire when the variables become set.
Understood. Personally, I would prefer the better "runtime behaviour" over the type safety here, but that's very opinionated.
In the future, you'll be able to do this in a more typesafe matter using skipToken
, which is already available with useSuspenseQuery
and which we will add to useQuery
in the future.
Generally, you're in a bit of an edge case situation regarding type safety - usually, you'd do something like this:
cache.modify<EntityType>({
id: ...,
fields: {
fieldName(valueOrRef) {
return differentValue
}
}
});
You can see more usage examples in our tests, e.g. at
apollo-client/src/cache/core/__tests__/cache.ts
Lines 348 to 383 in 9dc45bb
Your problem here is that your have deeper nesting here - which the cache will try to normalize, and if it's successful, nested fields like steps.edges
here will only be another reference.
So depending on how your cache can be normalized, you'd have to actually call cache.modify
for the instance stored in sub.data.conversationStepCreated.conversation.steps
instead, with the appropriate type for that.
from apollo-client.
Thansk a lot @phryneas this is helpful.
I'm trying to simplify the codebase to give a clearer example, as it seems that the question was misunderstood. Will come back to you soon guys :)
from apollo-client.
Ok, so I think I found a way to ask way, way more simply.
Imagine a Post
object with a connection
field comments
of type Comment
.
The client lists these comments by performing a "one shot" query to the node(id: postId)
field. Classic.
At the same time, it subscribes to something like CommentCreated(postId: ...)
which gives back a Comment
every time one is added to this given Post
identified by its id
.
I would imagine that the Apollo client cache would be configured with something like this:
new InMemoryCache({
fragments: createFragmentRegistry(),
typePolicies: {
Post: {
fields: {
comments: relayStylePagination(),
},
},
}
Cool.
You see - we were successful having these realtime comments added to the Post page - but we are adding them to a state array - not to the apollo cache backing the Post's comments connection. So when the user leaves the page and comes back, the state of the cache is shown without these new entries.
So the simplified question is: when the CommentCreated
subscription fires, how do you append that new post to the existing list in the Apollo cache (using TypeScript and with type guarantees)?
from apollo-client.
We've been successful with modifying the cache, with something like this - this being the function that is called when the subscription fires:
const onNewComment = (comment: DetailedCommentFragment) => {
cache.modify({
id: cache.identify({
__typename: "Post",
id: comment.post.id,
}),
fields: {
comments(existing) {
return {
...existing,
edges: [...existing.edges, { node: comment }],
};
},
},
});
The proble here is that existing
is any
. There are no guarantees at all, and it feels like blind luck that it works because it could in fact be a concrete CommentConnection
but it could also be a Reference
...
The moment we add a type hint to cache.modify<Post>(...)
to our code, it breaks because we're not handling Reference
... So we played around with readField
but can't seem to succeed with finding the right types to use.
Any sample code that would help deal with the Relay types (connections, edges having a cursor etc and appending to them) could be useful here.
In our case and translating Post
into Conversation
and Comment
into Step
with its connection on Post
being steps
:
// Whenever a subscription catches a new step, that's the handler.
const onNewStep = (step: DetailedStepFragment) => {
cache.modify<Conversation>({
id: cache.identify({
__typename: "Conversation",
id: step.conversation.id,
}),
fields: {
steps(existing, { readField }) {
const edges = readField("edges", existing) || [];
return {
...existing,
edges: [...edges, { node: step }],
};
},
},
});
This won't compile - because readField
gives us a string | number | void | Readonly<Object> | Readonly<Reference> | readonly string[] | readonly Reference[] | null | undefined
which doesn't match at all what we would expect - probably a ReadonlyArray<ConversationStepEdge>
?
But if we hard-code the hint (Eg readField<ReadonlyArray<ConversationStepEdge>>("edges", existing);
does it really guarantee that it can be that and only that?
Here's what happens when we try:
// Whenever a subscription catches a new step, that's the handler.
const onNewStep = (step: DetailedStepFragment) => {
cache.modify<Conversation>({
id: cache.identify({
__typename: "Conversation",
id: step.conversation.id,
}),
fields: {
steps(existing, { readField }) {
const edges = readField<ReadonlyArray<ConversationStepEdge>>(
"edges",
existing
);
return edges
? {
...existing,
edges: [...edges, { node: step }],
}
: existing;
},
},
});
The error is:
Type '(existing: Reference | AsStoreObject<ConversationStepConnection>, { readField }: ModifierDetails) => Reference | { ...; } | { ...; }' is not assignable to type 'Modifier<Reference | AsStoreObject<ConversationStepConnection>>'.\n Type 'Reference | { edges: (ConversationStepEdge | { node: DetailedStepFragment; })[]; __ref: string; } | { edges: (ConversationStepEdge | { ...; })[]; __typename?: \"ConversationStepConnection\" | undefined; pageInfo: PageInfo; }' is not assignable to type 'Reference | AsStoreObject<ConversationStepConnection> | DeleteModifier | InvalidateModifier'.\n Type '{ edges: (ConversationStepEdge | { node: DetailedStepFragment; })[]; __typename?: \"ConversationStepConnection\" | undefined; pageInfo: PageInfo; }' is not assignable to type 'Reference | AsStoreObject<ConversationStepConnection> | DeleteModifier | InvalidateModifier'.\n Type '{ edges: (ConversationStepEdge | { node: DetailedStepFragment; })[]; __typename?: \"ConversationStepConnection\" | undefined; pageInfo: PageInfo; }' is not assignable to type 'AsStoreObject<ConversationStepConnection>'.\n Types of property 'edges' are incompatible.\n Type '(ConversationStepEdge | { node: DetailedStepFragment; })[]' is not assignable to type 'ConversationStepEdge[]'.\n Type 'ConversationStepEdge | { node: DetailedStepFragment; }' is not assignable to type 'ConversationStepEdge'.\n Property 'cursor' is missing in type '{ node: DetailedStepFragment; }' but required in type 'ConversationStepEdge'.
Thanks a lot!
from apollo-client.
Also, looking at https://github.com/apollographql/apollo-client/blob/main/src/utilities/policies/pagination.ts#L94 it seems that this could do exactly what we're looking for - but we have no idea on how to use it after the subscription fires.
from apollo-client.
Ok - a bit of progress.
It seems that we're getting a bit closer to type safety, if we use cache.updateQuery
instead of cache.modify
.
Would this be a good way to achieve what we want? It seems to be working in a (very) controlled environment.
We were expecting that using cache.updateQuery
would let the field policy kick-in - so that we wouldn't need to do the merges ourselves. But it doesn't - so we ended up with:
Seems to us that it will break - because all the logic that relayStylePagination()
would do is completely bypassed.
WDYT?
const onNewStep = (step: DetailedStepFragment) => {
cache.updateQuery(
{
query: GQL.Conversation.StepsQuery,
variables: { id: step.conversation.id, last: 8 },
},
(existing) => {
if (existing?.node?.__typename !== "Conversation") return;
const node = frag(GQL.Conversation.WithSteps, existing.node);
return {
...existing,
node: {
...node,
steps: {
...node.steps,
edges: [
...node.steps.edges,
{ __typename: "ConversationStepEdge", node: step },
],
},
},
};
}
);
};
from apollo-client.
Related Issues (20)
- useQuery loading state always true HOT 7
- when apollo client query failed, it exit all the process and cannot catch the error HOT 1
- Field Policies: 'undefined' return value for `read` affects other field policy `read` return values. HOT 2
- nonreactive directive on a defer fragment HOT 1
- Apollo Client omits fields from fragments on an interfaces HOT 6
- Cannot reproduce Apollo Client 2 cache-and-network fetch policy HOT 3
- Undefined Unable to resolve module ../version.js from ...\node_modules\@apollo\client\core\ApolloClient.js HOT 4
- `fetchMore` race conditions when paginating after change to `useQuery` variables HOT 5
- Inconsistent behavior of `skip` option in `useQuery` leading to stale results HOT 1
- Uncaught TypeError: obsQuery.resubscribeAfterError is not a function HOT 1
- Missing statusCode in ApolloError causes inconsistent error handling in Nuxt HOT 2
- bad string formatting when constructing error - "Cannot convert object to primitive value" HOT 13
- bad string formatting when constructing error - "Cannot convert object to primitive value" HOT 2
- fetchMore loop on render does not update data HOT 2
- Using a setState inside a startTransition with a fetchMore cause Apollo to clear cached results when using a policy HOT 7
- Cannot read properties of undefined (reading 'bind') in useQuery hook HOT 3
- Performance regression in 3.9.x when updating cached data with identical data HOT 12
- How to do request in ssr component Strapi + Next js App Router with GraphQL + Apollo HOT 3
- Background query still active after component unmounted HOT 6
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from apollo-client.