blitz-js / babel-plugin-superjson-next

Automatically transform your Next.js Pages to use SuperJSON
MIT License
125 stars 15 forks source link

Issue when prefetching queries with React Query & NextJs #133

Closed codew0nderer closed 1 year ago

codew0nderer commented 1 year ago

Hello,

I would like to point out an issue when working with React Query and NextJs, that I don't think is the expected behavior, but correct me if I am wrong.

It occurs when prefetching queries using hydration. As mentioned in React Query docs, to prefetch using hydration the component in _app.tsx is wrapped in <Hydrate> which takes the pageProps.dehydratedState as prop.

Snippet to reproduce the behavior:

 // _app.jsx
 import { Hydrate, QueryClient, QueryClientProvider } from 'react-query'

 export default function MyApp({ Component, pageProps }) {
 // queryClient with a config to not immediately refetch client side
  const [queryClient] = React.useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            retry: false,
            refetchOnReconnect: false,
            refetchOnWindowFocus: false,
            staleTime: 20 * 1000 * 60, // 20 min
            cacheTime: 1 * 1000 * 30 * 60, // 30 min
          },
        },
      })
  );

   return (
     <QueryClientProvider client={queryClient}>
       <Hydrate state={pageProps.dehydratedState}>
         <Component {...pageProps} />
       </Hydrate>
     </QueryClientProvider>
   )
 }

But from my understanding since WithSuperJSON is wrapped around the page and not the _app.tsx component, that means that the pageProps.dehydratedState in_app.tsx is not deserialized, and as a result the <Hydrate> end up having a non deserialized dehydratedState which is saved in cache by React Query and later used by all useQuery hooks.

As a workaround to this, the pageProps could be explicitly deserialized in _app.tsx and passed to <Hydrate> like the following:

// _app.jsx
import { Hydrate, QueryClient, QueryClientProvider } from 'react-query'
import superjson from 'superjson';

 export default function MyApp({ Component, pageProps }) {
  const [queryClient] = React.useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            retry: false,
            refetchOnReconnect: false,
            refetchOnWindowFocus: false,
            staleTime: 10 * 1000 * 60, // 10 min
            cacheTime: 10 * 1000 * 60, // 10 min
          },
        },
      })
  );
  const { _superjson, ...props } = pageProps;
  const deserializedProps = superjson.deserialize({ json: props, meta: _superjson });

  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={deserializedProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  );
}

I can provide a git repository reproducing the behavior if the shared snippets are not enough.

Thank you!

Skn0tt commented 1 year ago

Hi @codew0nderer! We have a similar issue reported over in the SuperJSON repo: https://github.com/blitz-js/superjson/issues/196 Could you check if they're related?

Skn0tt commented 1 year ago

Wait, forget about that. Have you tried excluding the dehydratedState property from babel-plugin-superjson-next? See https://github.com/blitz-js/babel-plugin-superjson-next#options

codew0nderer commented 1 year ago

Hi, sorry for the late reply just had the opportunity to get back to this issue.

Excluding dehydratedState from serialization will not make use of SuperJson and will throw an error (image below) if the dehydratedState includes values that are not JSON-compatible (like Date values).

image

The issue here is that when the serialized state is passed to _app.tsx, pageProps.dehydratedState is still not deserialized at that point, which means that <Hydrate state={pageProps.dehydratedState> will be receiving a non-deserialized dehydratedState, and that will result in useQuery returning non-deserialized data (e.g: Date values as strings).

It is happening because the deserialization is done on page level as pages are wrapped with withSuperJSONPage, this can be noticed only with a large staleTime, so that react-query doesn't refetch immediately on client.

Snippets to reproduce the issue: _app.js

import { useState } from 'react';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';

function MyApp({ Component, pageProps }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            retry: false,
            refetchOnReconnect: false,
            refetchOnWindowFocus: false,
            staleTime: 10 * 1000 * 60, // 10 min - Long staleTime to not refetch immediately on client
            cacheTime: 10 * 1000 * 60, // 10 min
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  );
}

export default MyApp;

index.js

import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query';

const getPosts = () => ({ title: 'post', dateValue: new Date() });

export const getServerSideProps = async () => {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery(['posts'], getPosts);

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
      testDate: new Date(),
    },
  };
};

export default function Home(props) {
  const { data } = useQuery(['posts'], getPosts);

  // The value here should be date and not string
  console.log(data.dateValue); // '2022-12-07T09:41:20.924Z'
  console.log(data.dateValue instanceof Date); // false

  return <div>TEST</div>;
}

.babelrc

{
  "presets": ["next/babel"],
  "plugins": ["superjson-next"]
}
Skn0tt commented 1 year ago

Hey @codew0nderer, thanks for the explanation! Thank you for providing these steps. I'm not super well-versed with react-query, could you provide me with a ready-made reproduction repository where I just have to run npm run dev, some precise steps to reproduce, and an expected behaviour? That'd make it a lot easier for me to help you with this.

codew0nderer commented 1 year ago

Hello @Skn0tt ,

Thank you for your reply.

You can find in the following repository a reproduction of the issue.

The expected behavior is for data returned by useQuery in index.js:18 to have a dateValue of Date type and not string. I also included the workaround with comments if you would like to check it.

Let me know if you need any more details.

Thank you for your help 😊

Skn0tt commented 1 year ago

Thanks for providing the repro! Sorry for not replying earlier, was a bit under water ...

I tried setting it up, and running yarn install gave me error An unexpected error occurred: "http://verdaccio.lab.mwc.bc/@tanstack%2fquery-core/-/query-core-4.20.4.tgz: getaddrinfo ENOTFOUND verdaccio.lab.mwc.bc".. I think this is because of some special yarn config you have. I've removed the yarn.lock file locally and it works now - just thought i'd let you know, in case this turns out the be relevant for the bug.

Skn0tt commented 1 year ago

I see you have set up both babel-plugin-superjson-next and next-superjson-plugin in your project. These plugins do the same thing - one is a SWC plugin, one a babel plugin. Could you try removing one of them, and check if the error still exists?

codew0nderer commented 1 year ago

Indeed the resolve url was incorrect I have fixed that.

As for the dependencies I have both listed because I wanted to make sure that the issue is not happening just with the babel pluging, so I installed the SWC one and tested it as well, but it gave the same result.

I have deleted the SWC pluging from the reproduction repository dependencies and double checked again, the result is the same.

Let me know if you need any more details or help to reproduce or understand the issue.

Thank you for your time and help 😊

Skn0tt commented 1 year ago

Thanks for updating the repository! I was able to start the dev server. Seeing the code live made it a lot easier for me to understand what this issue is about :)

It looks like you have the same issue that was discussed in https://github.com/blitz-js/babel-plugin-superjson-next/issues/93, and you also found the same workaround :D The author of that original issue shipped a PR that adds a small tool which makes that easier to use: https://github.com/blitz-js/babel-plugin-superjson-next/pull/94

I've opened a PR to your reproduction repo to demonstrate how you might use it: https://github.com/codew0nderer/nextjs-superjson-playground/pull/1

I'm assuming this solves your issue, so I'll close. Feel free to re-open if this doesn't :)