Giter Club home page Giter Club logo

Comments (8)

phryneas avatar phryneas commented on May 24, 2024

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 the onData callback
  • please don't use useEffect to trigger a lazyQuery. useLazyQuery should respond to user interaction, not to React renders. You probably want to use useQuery with a skip option instead.

from apollo-client.

hickscorp avatar hickscorp commented on May 24, 2024

Good morning and thank you @phryneas .

  • We have a parent entity - Conversation that has a connection to edges of type Step.
  • We have a page listing Steps inside a Conversation. It has some logic to fetchMore by shifting over the PageInfo cursor. It's really just a query that goes something like query Steps(...) { node(id: ...) { ... on Conversation { steps(last: ..., before: ... { ... } ) } } }
  • We have a subscription that triggers when a new Step becomes part of a Conversation 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 the Step 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 in useEffect. 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, using useQuery along with the skip flag isn't an option, because we can't have this useQuery 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 that useQuery internally, it essentially results in the same internal logic with more complexity in our code.
  • Noted, very good to know. Thanks!

from apollo-client.

phryneas avatar phryneas commented on May 24, 2024

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

test("field types are inferred correctly from passed entity type", () => {
const cache = new TestCache();
cache.modify<{
prop1: string;
prop2: number;
child: {
someObject: true;
};
children: {
anotherObject: false;
}[];
}>({
fields: {
prop1(field) {
expectTypeOf(field).toEqualTypeOf<string>();
return field;
},
prop2(field) {
expectTypeOf(field).toEqualTypeOf<number>();
return field;
},
child(field) {
expectTypeOf(field).toEqualTypeOf<
{ someObject: true } | Reference
>();
return field;
},
children(field) {
expectTypeOf(field).toEqualTypeOf<
ReadonlyArray<{ anotherObject: false } | Reference>
>();
return field;
},
},
});
});

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.

hickscorp avatar hickscorp commented on May 24, 2024

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.

hickscorp avatar hickscorp commented on May 24, 2024

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.

hickscorp avatar hickscorp commented on May 24, 2024

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.

hickscorp avatar hickscorp commented on May 24, 2024

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.

hickscorp avatar hickscorp commented on May 24, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.