Data Fetching

Fetch server-side data inside components without prop drilling, and hydrate the client with zero additional network requests.

useServerData

useServerData(fn) lets any component fetch async data during SSR. The framework's render loop awaits the promise and re-renders the tree until every value is resolved, then serialises the results into the page JSON. On the client, the pre-resolved value is read from the hydration cache — fn is never called in the browser. The cache key is derived automatically from the call-site's position in the component tree via useId().

import { useServerData } from 'hadars';

const UserCard = ({ userId }: { userId: string }) => {
    const user = useServerData(() => db.getUser(userId));
    if (!user) return null; // undefined on the first SSR pass(es) while pending
    return <p>{user.name}</p>;
};

Client-side navigation

When a component mounts during client-side navigation and its data is not in the hydration cache, hadars fires a single GET <current-url> with Accept: application/json. All useServerData calls within the same React render are batched into one request and suspended until the server returns the JSON data map — regardless of how many keys are in the tree.

cache option

By default, fetched values are kept in the client cache for the lifetime of the page session. Pass { cache: false } to evict the entry when the component unmounts, so the next time it mounts it fetches fresh data from the server. This is useful for live values like uptime, server stats, or anything that changes between visits.

const uptime = useServerData(
    () => Math.round(process.uptime()),
    { cache: false },
);

The eviction is deferred by one macrotask so it is safe with React Strict Mode — Strict Mode's synchronous fake-unmount/remount cancels the timer before it fires, while a real unmount lets it run.

Synchronous data

fn can return a value synchronously. hadars detects non-Promise returns and stores them without suspending:

// Fine — fn returns synchronously, no suspense needed
const config = useServerData(() => ({
    theme: 'dark',
    version: process.env.APP_VERSION,
}));

React Query / Suspense hooks

hadars's SSR renderer natively supports the Suspense protocol — when a component throws a Promise (as Suspense-compatible hooks do), the renderer awaits it and retries automatically.

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

// getInitProps — create a per-request QueryClient
export const getInitProps = async (): Promise<Props> => ({
    rcClient: new QueryClient(),
    // ...
});

// getFinalProps — dehydrate the populated cache for the client
export const getFinalProps = async ({ rcClient, ...props }: Props) => {
    const cache = dehydrate(rcClient as QueryClient);
    return { ...props, cache };
};

// getClientProps — rehydrate on the client
export const getClientProps = async (props: PublicProps) => {
    const rcClient = new QueryClient({ defaultOptions: { queries: { staleTime: Infinity } } });
    hydrate(rcClient, props.cache);
    return { ...props, rcClient };
};

// Inside a component — useSuspenseQuery works SSR with no changes
const WeatherWidget: React.FC = () => {
    const { data } = useSuspenseQuery({
        queryKey: ['weather'],
        queryFn: () => fetch('/api/weather').then(r => r.json()),
        staleTime: Infinity,
    });
    return <p>{data.temperature} °C</p>;
};

The QueryClient cache is populated during SSR (slim-react awaits the thrown promises), dehydrated into the page JSON by getFinalProps, and rehydrated on the client by getClientProps. The hook returns synchronously on first client render — no second fetch.

Live demo

Three independent components each calling useServerData:

Hostname169.254.5.225
Process uptime461 s
NODE_ENVunknown

Navigate to the Data Fetch Demo page to see the batching behaviour — four keys resolved in a single request during client-side navigation.