apollographql / apollo-client-nextjs

Apollo Client support for the Next.js App Router
https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support
MIT License
440 stars 31 forks source link

Suspense Query not refetching (or updating) data after mutation! #368

Open siamahnaf opened 1 week ago

siamahnaf commented 1 week ago

I am using @apollo/client with nextjs package.

What I do in my code- page.tsx-

const Page = async () => {
    return (
        <PreloadQuery query={CATEGORY_LIST} variables={{ searchDto: defaultSearch }}>
            <Lists />
        </PreloadQuery>
    );
};

export default Page;

Here I do not use "Suspense" boundary because on the nextjs package documents, they said it is optional

Lists.tsx-

const Lists = () => {
    ....

    //Apollo
    const { data } = useSuspenseQuery(CATEGORY_LIST, { variables: { searchDto: pagination } });
    const [mutate, { loading }] = useMutation(DELETE_CATEGORY, {
        onCompleted: (data) => {
            toast.success(data?.deleteCategory.message);
            setConfirm(null);
        },
        onError: (error) => {
            toast.error(error.message)
        },
        refetchQueries: ["getCategories"],
        awaitRefetchQueries: true
    });
   ...

    return (
        ....
    );
};

export default Lists;

Here I add refetchQueries. It re-fetching the updating the data.

But from another component, Add.tsx and Edit.tsx from another page, I also add refetchQuerues with same queryName. In this case it's not updating the data.

But-

if I add fetchPolicy to network-only into this useSuspenseQuery then it updating data. That's means it not showing data from PreloadQuery component.

What is the best approach for my implementation. My only requirement is SSR data fetch, I don't want to show any loading screen or icon.

Here is my repository- https://github.com/siamahnaf/nextjs-apollo-client

If I use useQuery instead useQuerySuspense on that case refetch working perfect. But I am not understanding why I see a flash.

with useQuery-

https://github.com/user-attachments/assets/bd14585a-9d3d-4ee5-abb5-2a6a678b1d62

Here you can see a flash. That's means useQuery hook are not using data from PreloadQuery component.

with useSuspenseQueury

https://github.com/user-attachments/assets/99bfb114-3a9d-4864-b753-2e3e20312c1f

it is not giving flash, but data is not updating when refetch after mutation.

A outside question-

  1. useQuery, useSuspenseQuery has a queryKey parameter. are we can use this queryKey for refetch data from mutation?
  2. Is there any way to set errorPolicy for useSuspenseQuery in globally on ApolloClient creation.
phryneas commented 6 days ago

Hmm, regarding refetching, useQuery shouldn't behave differently from useSuspenseQuery. That's certainly weird. I'll bring this up with my colleagues, but unfortunately that will have to wait a few days as they're all out for a conference right now.

Afaik, you cannot use queryKey for refetching, but for now you could use one of the alternative notations for refetchQueries, e.g. refetchQueries: [CATEGORY_LIST], or refetchQueries: [{ query: CATEGORY_LIST, variables: { searchDto: pagination }}]

As for global errorPolicy for useSuspenseQuery: That is currently not possible, as we will give you different return types depending on your errorPolicy, and the moment you'd define that globally, we'd have to somehow react to that with the types, which is not possible in TypeScript.

siamahnaf commented 6 days ago

Thank you @phryneas. You are superstar, as you always provide us solutions.

Summary of my main issues, which can be discussed with your colleagues-

  1. useQuery hook is not picking data from PreloadQuery component as like useSuspenseQuery
  2. useSuspenseQuery is not refetching or updating data after mutation, when it's errorPolicy is it's default. Work when errorPolicy is set to network-only
phryneas commented 6 days ago

@siamahnaf on second thought, for 1., could you please check if there is actually a network request starting on the client? useQuery has to still wait for the data to actually arrive in the cache, so it goes got "loading", even if that loading happens on the server and is streamed over with PreloadQuery. It cannot "pause" rendering like useSuspenseQuery can.

phryneas commented 6 days ago

You just don't see it as an "active query" in the devtools, but that doesn't mean it doesn't fetch that data.

Take a look at the Cache tab of the devtools - it should appear there once it's fetched on the server.

siamahnaf commented 6 days ago

@phryneas, yes you are right. Sorry for misinformation. But I can share something with more behavior if you can understand it-

If I use directly useSuspenseQuery without wrapping PreloadQuery

const Page = async () => {
    return (
          <Lists /> //In this component I use useSuspenseQuery
    );
};

export default Page;

https://github.com/user-attachments/assets/8c8fdb14-0838-4b13-b6f1-c3141575032e

If I use PreloadQuery-

const Page = async () => {
    return (
        <PreloadQuery query={CATEGORY_LIST} variables={{ searchDto: defaultSearch }}>
            <div>Hello World</div>
        </PreloadQuery>
    );
};
export default Page;

https://github.com/user-attachments/assets/421849f0-e3b7-4b30-a550-d5dfe5160066

Cached data coming after first load. Is it any issue?

  1. Is PreloadQuery not running on ssr, that's why useQuery getting failed to pick data from cache into SSR?
siamahnaf commented 6 days ago

@phryneas,

I do another test-

const Page = () => {
    return (
        <PreloadQuery query={CATEGORY_LIST} variables={{ searchDto: { limit: 1 } }} errorPolicy="all" fetchPolicy="cache-first">
            <Lists />
        </PreloadQuery>
    );
};

export default Page;

Here I give limit: 1 as variable. On the other hand I give limit: 2 on useSuspenseQuery-

const { data } = useSuspenseQuery(CATEGORY_LIST, { variables: { searchDto: { limit: 2 } } });

See the following video

https://github.com/user-attachments/assets/6c61a098-0009-4e17-86b3-38cb3c297d58

useSuspenseQuery caching data before PreloadQuery Component.

**Updated I found something more-

PreloadQuery is not transport data into client component-

<script>
            (window[Symbol.for("ApolloSSRDataTransport")] ??= []).push({
                "rehydrate": {
                    ":R2pvj6:": {
                        "data": {
                            "getProfile": {
                                "__typename": "User",
                                "id": "2",
                                "name": "Siam Ahnaf",
                                "phone": "01611994403",
                                "email": "siamahnaf198@gmail.com",
                                "avatar": null,
                                "is_verified": true,
                                "role": "admin"
                            }
                        },
                        "networkStatus": 7
                    },
                    ":R4pvj6:": {
                        "data": {
                            "getProfile": {
                                "__typename": "User",
                                "id": "2",
                                "name": "Siam Ahnaf",
                                "phone": "01611994403",
                                "email": "siamahnaf198@gmail.com",
                                "avatar": null,
                                "is_verified": true,
                                "role": "admin"
                            }
                        },
                        "networkStatus": 7
                    }
                },
                "events": [{
                    "type": "started",
                    "options": {
                        "fetchPolicy": "cache-first",
                        "query": "query getProfile{getProfile{id name phone email avatar is_verified role}}",
                        "notifyOnNetworkStatusChange": false,
                        "nextFetchPolicy": undefined
                    },
                    "id": "1"
                }, {
                    "type": "data",
                    "id": "1",
                    "result": {
                        "data": {
                            "getProfile": {
                                "__typename": "User",
                                "id": "2",
                                "name": "Siam Ahnaf",
                                "phone": "01611994403",
                                "email": "siamahnaf198@gmail.com",
                                "avatar": null,
                                "is_verified": true,
                                "role": "admin"
                            }
                        }
                    }
                }, {
                    "type": "complete",
                    "id": "1"
                }]
            })
        </script>

Here is no category information,

const Page = async () => {
    return (
        <PreloadQuery query={CATEGORY_LIST} variables={{ searchDto: defaultSearch }}>
            <div></div>
        </PreloadQuery>
    );
};

export default Page;

No category list is transporting.

phryneas commented 6 days ago

PreloadQuery uses a different transport mechanism, it doesn't inject HTML into your page, it uses a Promise prop, which is then transported by React.

Turn on debugging via

import { setVerbosity } from "ts-invariant";
setVerbosity("debug");

to see the data transported.

And yes, if you query for two different things in PreloadQuery and useSuspenseQuery, those will transport independently from each other. But if you query both for the same thing and useSuspenseQuery is a child of PreloadQuery, useSuspenseQuery will not start an additional network request.

siamahnaf commented 6 days ago

@phryneas, then I don't know. But issues is useQuery is not picking data from PrealoadQuery on SSR, instead it pick after page load.

I am seeing that, useQuery not placing any network request.

phryneas commented 6 days ago

useQuery will usually not pick up any data during SSR because SSR renders only once and useQuery cannot "pause" your render until a response has finally arrived - it has to work synchronously.

The only way useQuery will pick up data during SSR is if you deliberately pause the rendering using suspense in a parent component until the data is there and only after that start rendering the component calling useQuery - but that's kinda flimsy. If you really need this to cleanly be SSRed, you probably cannot rely on a non-suspense API like useQuery - that only really works in the browser.

siamahnaf commented 6 days ago

I found one solutions, as your previous text. It's my last question, please don't mind-

I am showing list, on list page. And I am creating record from add-new page

Previously I just try to refetch list page useSuspenseQuery from add-new page mutation like following way-

const [mutate, { loading }] = useMutation(ADD_CATEGORY, {
        onCompleted: (data) => {
            toast.success(data.addCategory.message);
            reset();
        },
        onError: (error) => {
            toast.error(error.message)
        },
        refetchQueries: [CATEGORY_LIST],
        awaitRefetchQueries: true
});

But now I change to like-

refetchQueries: [{ query: CATEGORY_LIST, variables: { searchDto: defaultSearch } }]

It's now refetching data and updating useSuspenseQuery cached.

Questions why previously it was not working.

It's my last questions, As I get solutions.

phryneas commented 6 days ago

I don't know right now, I'll have to check in with my colleagues on that one next week. It should refetch.

siamahnaf commented 6 days ago

Okay, thanks you are very helping man. By getting your help, at least I get one solutions. For keeping minimal code, it should refetch with only the queryName or query itself. So I will wait for your next response in next week. Thank you.

phryneas commented 3 hours ago

Yeah, all we can say is that this really should refetch and we've never had reports of this problem before. Could you maybe create a smaller, self-contained version of the problem that we can run ourselves and experiment in?