Sitelet https://github.com/ReactLibraries/react-query-ssr
Skip to content

Repository files navigation

react-query-ssr

Ask DeepWiki

overview

SSR functionality with react-query.

Provides SSR functionality from Next.js‘s Pages Router and App Router’s Client Component. It also works with React Router.

SSR does not use the functionality of the respective frameworks and works only with standard React functionality. Therefore, it does not require any environment.

URL of sample program

https://next-react-query-ssr.vercel.app/
https://github.com/SoraKumo001/next-react-query-ssr

Comparison with Official HydrationBoundary

react-query-ssr provides an alternative hydration approach compared to the official @tanstack/react-query's HydrationBoundary.

Feature Official HydrationBoundary react-query-ssr (SSRProvider)
Prefetch Implementation Explicit: Requires calling await queryClient.prefetchQuery(...) on the server before rendering. Implicit / Automatic: Automatically detects Promises inside components during SSR and waits for them.
Data Transfer Mechanism React Component Props: Passes dehydrated state via props (state prop), which is serialized into RSC payload / page props. HTML <script> tag: Injects JSON.stringifyed state directly into a <script id="__REACT_QUERY_DATA_PROMISE__" type="application/json"> tag.
Code Verbosity Higher: Requires writing extra data-fetching code on the server-side. Lower: No server-specific fetching code is needed. Components with useQuery work directly out-of-the-box.
Best Suited For - Standard RSC (App Router) environments.
- Pages Router with explicit prefetching APIs (getServerSideProps).
- Simplified SSR setups where you want to write components once and run them on both server/client without duplicate fetch logic.

How react-query-ssr works under the hood

Instead of requiring manual prefetching on the server, react-query-ssr intercepts pending promises generated by useQuery during the SSR rendering cycle. If there are unresolved queries, it throws a Promise.all of these pending promises, triggering React's Suspense cycle on the server to wait for data resolution. Once resolved, it renders the HTML and injects the cache into a <script> tag for the client to hydrate automatically.

Notes.

Do not use <Suspense> for components using useSSR in ServerSide. You will not be able to wait for asynchronous data.

Example

  • app/Provider.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { FC, ReactNode, useState } from "react";
import { SSRProvider } from "react-query-ssr";

export const Provider: FC<{ children: ReactNode }> = ({ children }) => {
  const [queryClient] = useState(
    () =>
      new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000 } } })
  );
  return (
    <QueryClientProvider client={queryClient}>
      <SSRProvider>{children}</SSRProvider>
    </QueryClientProvider>
  );
};
  • app/Layout.tsx
import { Provider } from "./Provider";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Provider>{children}</Provider>
      </body>
    </html>
  );
}
  • src/app/[page]/page.tsx
"use client";
import { useQuery } from "@tanstack/react-query";
import { enableSSR } from "react-query-ssr";

import Link from "next/link";
import { useParams } from "next/navigation";

type PokemonList = {
  count: number;
  next: string;
  previous: string | null;
  results: { name: string; url: string }[];
};

const pokemonList = (page: number): Promise<PokemonList> =>
  fetch(`https://pokeapi.co/api/v2/pokemon/?offset=${(page - 1) * 20}`).then(
    (r) => r.json()
  );

const Page = () => {
  const params = useParams();
  const page = Number(params["page"] ?? 1);
  const { data } = useQuery({
    // `useQuery` with this option.
    ...enableSSR,
    queryKey: ["pokemon-list", page],
    queryFn: () => pokemonList(page),
  });

  if (!data) return <div>loading</div>;
  return (
    <>
      <title>Pokemon List</title>
      <div style={{ display: "flex", gap: "8px", padding: "8px" }}>
        <Link
          href={page > 1 ? `/${page - 1}` : ""}
          style={{
            textDecoration: "none",
            padding: "8px",
            boxShadow: "0 0 8px rgba(0, 0, 0, 0.1)",
          }}
        >
          ⏪️
        </Link>
        <Link
          href={page < Math.ceil(data.count / 20) ? `/${page + 1}` : ""}
          style={{
            textDecoration: "none",
            padding: "8px",
            boxShadow: "0 0 8px rgba(0, 0, 0, 0.1)",
          }}
        >
          ⏩️
        </Link>
      </div>
      <hr style={{ margin: "24px 0" }} />
      <div>
        {data.results.map(({ name }) => (
          <div key={name}>
            <Link href={`/pokemon/${name}`}>{name}</Link>
          </div>
        ))}
      </div>
    </>
  );
};
export default Page;
  • src/app/pokemon/[name]/page.tsx
"use client";
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { useParams } from "next/navigation";
import { enableSSR } from "../../react-query-ssr";

type Pokemon = {
  abilities: { ability: { name: string; url: string } }[];
  base_experience: number;
  height: number;
  id: number;
  name: string;
  order: number;
  species: { name: string; url: string };
  sprites: {
    back_default: string;
    back_female: string;
    back_shiny: string;
    back_shiny_female: string;
    front_default: string;
    front_female: string;
    front_shiny: string;
    front_shiny_female: string;
  };
  weight: number;
};

const pokemon = (name: string): Promise<Pokemon> =>
  fetch(`https://pokeapi.co/api/v2/pokemon/${name}`).then((r) => r.json());

const Page = () => {
  const params = useParams();
  const name = String(params["name"]);
  const { data } = useQuery({
    // `useQuery` with this option.
    ...enableSSR,
    queryKey: ["pokemon", name],
    queryFn: () => pokemon(name),
  });

  if (!data) return <div>loading</div>;
  return (
    <>
      <title>{name}</title>
      <div style={{ padding: "8px" }}>
        <Link
          href="/1"
          style={{
            textDecoration: "none",
            padding: "8px 32px",
            boxShadow: "0 0 8px rgba(0, 0, 0, 0.1)",
            borderRadius: "8px",
          }}
        >
          ⏪️List
        </Link>
      </div>
      <hr style={{ margin: "24px 0" }} />
      <div
        style={{
          display: "inline-flex",
          flexDirection: "column",
          alignItems: "center",
          padding: "8px",
        }}
      >
        <img
          style={{ boxShadow: "0 0 8px rgba(0, 0, 0, 0.5)" }}
          src={data.sprites.front_default}
        />
        <div>{name}</div>
      </div>
    </>
  );
};
export default Page;

About

SSR functionality with react-query.

Resources

Stars

8 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages