SSR & stores
atomic-query is client-only. Server-side data fetching is your framework’s job. The seam between the two is <HydrateQueries>: your loader fetches, you pass the result to the component, the cache picks it up on first render.
<HydrateQueries>
import { HydrateQueries, type SeedEntry } from "@mongez/atomic-query";
<HydrateQueries entries={[ { queryKey: ["users"], data: usersFromLoader }, { queryKey: ["currentUser"], data: currentUserFromLoader, freshFor: 60_000 },]}> <App /></HydrateQueries>Each entry seeds the cache with state: "success", isLoading: false. Consumers using queryAtom.useQuery({ queryKey: ["users"], queryFn, staleTime: 60_000 }) see the seeded data on first render — no flash, no spinner, no refetch as long as it’s fresh.
freshFor (optional) overrides the consumer’s staleTime for this entry.
Next.js (App Router)
// app/users/page.tsx — server componentimport { HydrateQueries } from "@mongez/atomic-query";import { UserListClient } from "./UserListClient";
export default async function Page() { const users = await db.users.findMany(); return ( <HydrateQueries entries={[{ queryKey: ["users"], data: users }]}> <UserListClient /> </HydrateQueries> );}"use client";import { queryAtom } from "@mongez/atomic-query";
export function UserListClient() { const { data } = queryAtom.useQuery<User[]>({ queryKey: ["users"], queryFn: ({ signal }) => fetch("/api/users", { signal }).then(r => r.json()), staleTime: 60_000, }); return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;}Remix
import { json, useLoaderData } from "@remix-run/react";import { HydrateQueries } from "@mongez/atomic-query";import { UserList } from "~/components/UserList";
export async function loader() { return json({ users: await db.users.findMany() });}
export default function UsersRoute() { const { users } = useLoaderData<typeof loader>(); return ( <HydrateQueries entries={[{ queryKey: ["users"], data: users }]}> <UserList /> </HydrateQueries> );}TanStack Start
import { createFileRoute } from "@tanstack/start";import { HydrateQueries } from "@mongez/atomic-query";
export const Route = createFileRoute("/users")({ loader: async () => ({ users: await db.users.findMany() }), component: UsersRoute,});
function UsersRoute() { const { users } = Route.useLoaderData(); return ( <HydrateQueries entries={[{ queryKey: ["users"], data: users }]}> <UserList /> </HydrateQueries> );}What about useSuspenseQuery?
atomic-query does ship useSuspenseQuery — see mongez-atomic-query-suspense for the dedicated skill. It’s a client-side concern though: it suspends a subtree while a query loads, after the initial render. For streaming SSR (the initial render itself), the suspense boundary belongs in your framework:
- Next.js: do the await in a server component; the suspense boundary belongs there.
- Remix:
defer()+<Await>. - TanStack:
pendingComponenton the route.
The common pattern is: framework loader fetches → <HydrateQueries> seeds the cache → client components use queryAtom.useQuery (or useSuspenseQuery if you want a client-side suspense boundary on top of the seeded data).
What about prefetchQuery?
For SSR, fetch in the framework loader instead — same effect, fewer moving parts.
For client-side prefetching (e.g., hover-over-link), fire the queryFn manually and call seedQuery:
async function prefetchUser(id: number) { const user = await api.users.get(id); queryAtom.seedQuery({ queryKey: ["users", id], data: user });}
<Link onMouseEnter={() => prefetchUser(user.id)} href={`/users/${user.id}`}> ...</Link>