GioJSdocs
On this page

Fetching Data

Load data on the server with getServerSideProps.

Export an async getServerSideProps from a page to fetch data on the server before render. The returned props are passed to your component.

tsx
export default function Post({ post }) {
  return <article><h1>{post.title}</h1></article>;
}

export async function getServerSideProps(ctx) {
  const post = await db.posts.find(ctx.params.id);
  return { props: { post } };
}

Types

Type the loader with GetServerSideProps from @gio.js/core: the first type argument is the page's props, the second the route pattern, which types ctx.params. The pattern is checked against the routes the server discovered (the generated .gio/routes.d.ts), so a typo fails tsc; a params shape ({ id: string }) works too. The result must be one the server accepts: { props } (optionally with headers and tags), a redirect, { notFound: true } or redirect().

tsx
import type { GetServerSideProps } from '@gio.js/core';

interface Props {
  post: Post;
}

export const getServerSideProps: GetServerSideProps<Props, '/posts/:id'> = async (ctx) => {
  const post = await db.posts.find(ctx.params.id);   // ctx.params: { id: string }
  if (!post) return { notFound: true };
  return { props: { post } };
};

export default function PostPage({ post }: Props) {
  return <article><h1>{post.title}</h1></article>;
}

The component receives exactly the returned props - not the params. InferPageProps<typeof getServerSideProps> reads them off an unannotated loader. A page without getServerSideProps receives { params, searchParams } instead: type it as PageProps<'/posts/:id'>. In JavaScript the same types work through JSDoc, as in the default-js starter: /** @type {import('@gio.js/core').GetServerSideProps<{ post: Post }, '/posts/:id'>} */. All types are listed under Functions.

Redirects

Return a redirect instead of props to send the visitor elsewhere.

tsx
return { redirect: { destination: '/login', permanent: false } };

The redirect() helper from @gio.js/core - the one page actions use - works here too: return it, or throw it from anything getServerSideProps calls, so one guard serves pages and actions alike. It answers 303 See Other unless you pass another status, and is never cached.

tsx
import { redirect } from '@gio.js/core';

// lib/auth.server.ts - shared by getServerSideProps and actions
export function requireUser(cookies: Record<string, string>) {
  const user = readSession(cookies);
  if (user === null) throw redirect('/login');
  return user;
}

export async function getServerSideProps(ctx) {
  const user = requireUser(ctx.cookies);
  return { props: { user } };
}

After a form post

When a page's action re-renders it (a validation error, say), getServerSideProps runs for that POST too, with the action's result in ctx.actionData; the page component gets it as the actionData prop. Neither render is ever cached. Headers the action returned (a cookie) are sent whatever answers in the end - the page, or a redirect or 404 from getServerSideProps. See Forms and Mutations.

tsx
export async function getServerSideProps(ctx) {
  const post = await db.posts.find(ctx.params.id);
  // Keep the comment the visitor typed when the action rejected it.
  const draft = ctx.actionData?.draft ?? '';
  return { props: { post, draft } };
}

Not found

When the data does not exist, call notFound() - or return { notFound: true }. The page answers 404 with the nearest not-found.tsx at or above its folder (see Error Handling).

tsx
import { notFound } from '@gio.js/core';

export async function getServerSideProps(ctx) {
  const post = await db.posts.find(ctx.params.id);
  if (!post) notFound();             // or: return { notFound: true };
  return { props: { post } };
}

notFound() works by throwing, so a try/catch around it swallows it - call it outside the try, or rethrow. It works while rendering too, and in route.ts handlers (a JSON 404). A 404 is never cached, even with revalidate set.

Response headers and cookies

Return headers next to props (or a redirect) to set response headers. Pass an array to set-cookie to set several cookies - each becomes its own Set-Cookie header. An array for any other header is joined with , .

tsx
export async function getServerSideProps(ctx) {
  const session = await refreshSession(ctx.cookies['session']);
  return {
    props: { user: session.user },
    headers: {
      'set-cookie': [
        `session=${session.id}; Path=/; HttpOnly; Secure; SameSite=Lax`,
        `csrf=${session.csrf}; Path=/; Secure; SameSite=Strict`,
      ],
    },
  };
}

// app/logout/page.tsx - clear both cookies on the way out
export async function getServerSideProps() {
  return {
    redirect: { destination: '/', permanent: false },
    headers: { 'set-cookie': ['session=; Path=/; Max-Age=0', 'csrf=; Path=/; Max-Age=0'] },
  };
}

A page that returns headers is never cached, even with revalidate set - caching a per-request cookie would hand one visitor's session to everyone.

Cache tags

On a cached page, return tags next to props to name the data this render used; revalidateTag() then purges exactly the pages that showed it - see Caching.

tsx
export const revalidate = 3600;

export async function getServerSideProps(ctx) {
  const post = await db.posts.find(ctx.params.id);
  return { props: { post }, tags: [`post:${post.id}`] };
}
Never fetch data inside the component body - it runs during SSR and inflates time-to-first-byte. Use getServerSideProps.

Cookies and caching

ctx.cookies and ctx.headers carry the visitor's request. Reading ctx.cookies or the cookie/authorization headers marks the render as personalized, so a page that also exports revalidate is not cached for that request - see Caching.

ctx.ip is the visitor's IP address (proxy-aware: behind a reverse proxy it needs [server] trusted_proxies, see Configuration). Reading it marks the render as personalized exactly like reading a cookie - a page that varies by IP (geo, an allowlist) must never be cached and served to everyone - and so does reading the raw x-forwarded-for, forwarded or x-real-ip headers.

ctx.host and ctx.scheme (the host and scheme the client used) mark the render too, as do the raw host, x-forwarded-host and x-forwarded-proto headers. The host is whatever the client sent unless your proxy pins it: a cached page that printed it - an absolute link, a canonical URL - would hand one request's Host: evil.example to every later visitor. For absolute URLs on cached pages, use an origin you configure (an environment variable) instead. Only ctx.requestId (the response's X-Request-Id, for logs and downstream calls) does not mark the render: it never shapes the page.

The same goes for the context generateMetadata gets: reading its cookies, credential headers, IP, host or scheme marks the render as personalized too, because the tags it returns are part of the page. With PPR that includes metadata built from the props of a getServerSideProps that read them: the <head> is in the cached shell.

tsx
export const revalidate = 60;

export async function getServerSideProps(ctx) {
  const posts = await db.posts.latest({ requestId: ctx.requestId }); // still cached
  const canonical = `${process.env.SITE_URL}/posts`;              // not ctx.host
  return { props: { posts, canonical } };
}

ctx.headers is a tracked view of the request headers (a Proxy), so structuredClone, postMessage and worker threads reject it. Pass a plain copy instead - { ...ctx.headers } - which, like any enumeration of the headers, counts as reading them.