apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.
https://apollographql.com/client
MIT License
19.33k stars 2.65k forks source link

Question about using useTransition with useSuspenseQuery's refetch and fetchMore after SSR #11590

Open sixone0712 opened 7 months ago

sixone0712 commented 7 months ago

Hello,

I have a question regarding the use of useTransition with useSuspenseQuery's refetch and fetchMore functionalities.

After server-side rendering (SSR), I'm trying to use startTransition from useTransition on the client side to perform refetch and fetchMore operations. My goal is to fetch new data without rendering the Suspense fallback component, thereby keeping the existing data displayed.

When I use startTransition, it successfully fetches the data without rendering the Suspense fallback component, as intended. However, I've encountered an issue where useTransition's isPending state does not change from true to false after the data has been fetched.

As a temporary workaround, I've added logic to manually change the state after refetch or fetchMore to ensure that isPending turns to false after loading the data. Here's the code snippet for reference:

function useQueryProduct(code: string) {
  return withTransition(useSuspenseQuery(GET_PRODUCT_DETAIL, { variables: { productCode: code } }));
}

function withTransition<TData>(props: UseSuspenseQueryResult<TData, OperationVariables>) {
  const [_, updateState] = useState([]);
  const forceUpdate = useCallback(() => updateState([]), []);
  const [isPending, startTransition] = useTransition();
  const { refetch, fetchMore, ...rest } = props;

  const refetchWithTransition = useCallback(() => {
    startTransition(() => {
      refetch().finally(() => {
        forceUpdate();
      });
    });
  }, []);

  const fetchMoreWithTransition = useCallback((...fetchMoreOptions: Parameters<typeof fetchMore>) => {
    startTransition(() => {
      fetchMore(...fetchMoreOptions).finally(() => {
        forceUpdate();
      });
    });
  }, []);

  return { ...rest, refetch, fetchMore, refetchWithTransition, fetchMoreWithTransition, isPending } as const;
}

Is there a better approach to handling this issue? Or is there a possibility that the behavior of isPending not changing from true to false will be addressed in a future update?

Thank you for your assistance.

phryneas commented 7 months ago

I believe that's an Apollo Client bug, so I'll transfer it over :)

sixone0712 commented 7 months ago

Thank you for your quick response. :)

However, when I tested with the Suspense example provided by Apollo Client, it worked as expected.

For your reference, the example I tested is as follows:

apollo client suspense example

joaquimds commented 2 months ago

I have the same problem, using NextJS. However, using @apollo/client packages (instead of @apollo/experimental-nextjs-app-support packages) did not fix the issue.

joaquimds commented 2 months ago

I was able to fix this for myself by putting useTransition() as the first line of my component function:

export default function Space({
  params: { slug },
}: {
  params: { slug: string };
}) {
  const [isPending, startTransition] = useTransition();
  const { data, fetchMore } = useSuspenseQuery<SpaceQuery, SpaceQueryVariables>(
    SPACE_QUERY,
    {
      variables: { slug },
    },
  );
  ...
}

I think in general useTransition has to be called before any hook that might cause a component to be suspended. This is to ensure that useTransition is called on every render/update. Hooks that suspend throw exceptions to interrupt code execution, so if the useTransition is after useSuspenseQuery, it is not always called.

hao-cro-matic commented 6 days ago

I am having the same problem. I am preloading the query outside React and trying to fetch more data using fetchMore from useQueryRefHandlers. isPending state didn't change from true to false after the data is fetched. Here's the code snippet:

const Notifications = () => {
  const [isPending, startTransition] = React.useTransition();
  const { notificationsQueryRef } = useLoaderData<typeof notificationQueryLoader>(); // Getting query ref from React Router data loader
  const { data: queryData } = useReadQuery(notificationsQueryRef);
  const notificationEdges = queryData?.notificationsConnection?.edges || [];
  const notificationPageInfo = queryData?.notificationsConnection?.pageInfo;
  const { fetchMore, subscribeToMore } = useQueryRefHandlers(notificationsQueryRef);
  const handleFetchMore = React.useCallback(() => {
    startTransition(() => {
      fetchMore({
        variables: {
          after: notificationPageInfo!.endCursor,
          first: 15,
        },
      });
    });
  }, [notificationPageInfo, fetchMore]);

  ...
}

@sixone0712 Thanks for the temporary walkaround. It is really helpful!

The versions I am using: "@apollo/client": "^3.11.8" "react": "^18.2.0" "react-router-dom": "^6.26.1"