jotaijs / jotai-tanstack-query

Jotai integration library for TanStack Query
MIT License
200 stars 14 forks source link

Tanstack hydrating order, Jotai first, or Tanstack first? #37

Open davit-b opened 1 year ago

davit-b commented 1 year ago

I've created a React SPA using Next13 appDir, Jotai, and Tanstack.

Question: What order do I hydrate in?

Option A:

  1. Initial datafetch in server component
  2. pass as props to a client component
  3. ...which hydrates a Jotai atom using useHydrateAtoms
  4. the atomsWithQuery automatically get's hydrated with initialData
    
    // atoms.ts
    export const dataAtom = atom<DataType | null>(null)
    export const queryAtom = atomsWithQuery(get => ({
    queryKey: QUERY_KEY,
    queryFn: fetchingFunction,
    initialData: get(dataAtom)
    }))
    //--------------------------------

// app/page.tsx [SERVER] export default async function Page() { const data = getUser()

return (

) }

// /components/HydratorComponent 'use client' import React from 'react'

function HydratorComponent({initialDataSentFromServer}) { useHydrateAtoms([[dataAtom, initialDataSentFromServer]]) return (

) }

#### TL;DR: SERVER (fetch) -> CLIENT (as props) -> useHydrateAtom (a basic data atom) -> Query Atom gets populate with `initialData`

-- or --

### Option B:
1. define a queryAtom without initialData
2. initial data fetch on server component
3. pass the initial data as props to client component
5. ...which hydrates the query atom using `useHydrateAtoms([[dataAtom, initialDataSentFromServer]])`
```ts
// atoms.ts
export const queryAtom = atomsWithQuery((get) => ({
  queryKey: QUERY_KEY,
  queryFn: fetchingFunction,
}))
//--------------------------------

// app/page.tsx [SERVER]
export default async function Page() {
  const data = getUser()

  return (
    <div>
      <HydratorComponent initialDataSentFromServer={data} />
      <RestOfTheApp />
    </div>
  )
}

// /components/HydratorComponent
'use client'
import React from 'react'

function HydratorComponent({ initialDataSentFromServer }) {
  useHydrateAtoms([[queryAtom, initialDataSentFromServer]])
  return <div></div>
}

TL;DR: SERVER (fetch) -> CLIENT (as props) -> useHydrateAtom (the queryAtom)

I'm a beginner and I really like Jotai. Would love to figure out the best practice for this.

dai-shi commented 1 year ago

Can anyone help here?

I never tried, but I don't think atoms created by jotai-tanstack-query should be hydrated on the Jotai end.

estubmo commented 1 year ago

This is my setup:

providers.tsx

"use client";

import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
import { Provider } from "jotai";

import HydrateQueryClientAtom from "~/components/HydrateQueryClientAtom"

export default function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = React.useState(() => new QueryClient());

  return (
      <QueryClientProvider client={queryClient}>
        <ReactQueryStreamedHydration>
          <Provider>
            <HydrateQueryClientAtom queryClient={queryClient}>
              {children}
            </HydrateQueryClientAtom>
          </Provider>
        </ReactQueryStreamedHydration>
        <ReactQueryDevtools initialIsOpen={true} />
      </QueryClientProvider>
  );
}

Providers wrap my entire app in the root layout.tsx.

HydrateQueryClientAtom.tsx

"use client";

import type { QueryClient } from "@tanstack/query-core";
import { queryClientAtom } from "jotai-tanstack-query";
import { useHydrateAtoms } from "jotai/utils";

const HydrateQueryClientAtom = ({ children, queryClient }: { children: React.ReactNode; queryClient: QueryClient }) => {
  useHydrateAtoms([[queryClientAtom, queryClient]]);

  return children;
};

export default HydrateQueryClientAtom;

And then in any page wrapped by this layout I can go something like:

import { dehydrate } from "@tanstack/query-core";
import { HydrationBoundary } from "@tanstack/react-query";

async function preFetchSomeData() {
  const queryClient = getQueryClient();
  await queryClient.prefetchQuery({ queryKey: ["key", params], queryFn: dataQueryFunction });
  const dehydratedState = dehydrate(queryClient);

  return dehydratedState
}

export default async function SomePage() {
  const dehydratedState = await preFetchSomeData()

  return (
    <HydrationBoundary state={dehydratedState}>
      // ...
    </HydrationBoundary>
  );
}

I hope that answers your questions.

davit-b commented 1 year ago

@estubmo Extremely helpful.

Similar to my Tanstack-only setup right now.

export default async function HydratedApp() {
  const data = await getDataServerSide() // server-side data fetching function

  const queryClient = getQueryClient()
  queryClient.setQueryData(['QUERY_KEY'], data)
  const dehydratedState = dehydrate(queryClient)

  return (
      <Hydrate state={dehydratedState}>
        <App /> // The `use client` wrapped SPA in inside of <App />
      </Hydrate>
  )
}
  // Inside a component deep in the tree.
 const { data: userData } = useQuery({
    queryKey: ['QUERY_KEY'],
    queryFn: getDataClientSide //client-side datafetching function
  })

BUT with Tanstack-only I have to access data client-side using Tanstack's convention, and my React components are subscribed using Tanstack's hooks — and I can't use the magic of Jotai for deriving atoms or anything off that data.


Before I ask any followups, I'm going to experiment with this setup and report my findings. Thank you!

estubmo commented 1 year ago

You're very welcome. I updated the example, as it was missing an essential part, calling queryClient.prefetch(). I think that might be a more suitable API than setQueryData. https://tanstack.com/query/v4/docs/react/reference/QueryClient#queryclientprefetchquery