react-query-ssr
v1.0.4
Published
Library for easy SSR of data obtained from asynchronous in Next.js.
Maintainers
Readme
react-query-ssr
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;