GioJSdocs
On this page

getServerSideProps

Load a page's data on the server for each render, and answer with props, a redirect, a 404 or response headers.

app/posts/[id]/page.tsx
import { notFound, type GetServerSideProps, type InferPageProps } from '@gio.js/core';
import { db, type Post } from '../../../lib/db.server.ts';

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

export default function PostPage({ post }: InferPageProps<typeof getServerSideProps>) {
  return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}

Export an async getServerSideProps from a page.tsx. The Node worker calls it before it renders the page, and the page component receives exactly the props it returned. It runs only on the server: the browser bundle imports the page's default export alone, so this function and the modules only it imports are left out. It is read from pages only; a layout cannot export one.

Reference

Parameters

ctx (GsspContext, also exported as GetServerSidePropsContext) describes the request:

FieldTypeDefaultDescription
methodstring-'GET' or 'HEAD', or 'POST' on the render that answers a page action.
pathstring-The routed path, without the query string or a locale prefix (after [[rewrites]]).
paramsParamsOf<Route>-The dynamic segments: [id] gives { id: string }. A catch-all is one /-joined string ('a/b'), and an optional catch-all that matched nothing is ''.
queryRecord<string, string>-The query string, one value per name: the last one when a name repeats.
headersRecord<string, string>-The request headers, names lowercase. A tracked view (a Proxy): see personalized renders.
cookiesRecord<string, string>-The Cookie header, parsed. Reading it makes the render personal.
localestring | undefined-The request locale with [i18n] configured; absent without it.
ipstring | undefined-The client's address, proxy-aware ([server] trusted_proxies). Reading it makes the render personal.
schemestring | undefined-'https' or 'http', as the client used it. Reading it makes the render personal.
hoststring | undefined-The host the client addressed, which is whatever the client sent unless a proxy pins it. Reading it makes the render personal.
requestIdstring | undefined-This request's X-Request-Id, also on every log line. Reading it does not make the render personal.
actionDataunknown-The page action's result, on the render that answers a POST; absent on every GET.

Returns

Return (or resolve to) one of these:

ValueAnswer
{ props, headers?, tags? }200 with the page rendered from props. headers are response headers; tags are cache tags for this render.
Any other objectUsed as the props themselves ("flat props"). A headers or tags key in it is just a prop.
{ redirect: { destination, permanent }, headers? }301 when permanent is true, else 302, with Location: destination. headers go with it. A destination that redirect() would refuse (not a string, empty, or holding a control character such as a newline) is a render error, answered 500, never sent.
redirect(url, init?)A 303 by default, or the status you pass (301, 302, 307, 308), with init.headers. See redirect.
{ notFound: true }404 with the nearest not-found.tsx, the same as calling notFound().

Header values are strings or string arrays. Each set-cookie entry is sent as its own Set-Cookie header; an array for any other header is joined with , . Names are matched case-insensitively. An empty 'set-cookie': [] sends nothing and does not count as headers.

Behavior

  • When it runs. On every render of the page: each request to a page without revalidate, and each cache miss or background refresh of a cached page. A cache hit is served by the Rust server without calling it. On a PPR shell hit it runs again for the holes, with the visitor's own cookies. Under gio export it runs once per exported page, at build time, with empty headers, cookies and query.
  • Thrown answers. notFound() and redirect() may be thrown from getServerSideProps or anything it calls; they answer like the returned forms. Any other error answers 500 with the nearest error.tsx, its details in the server log under the response's digest.
  • Bad results. A result that is not an object (null, a string, an array) is a render error: getServerSideProps for route "/x" must return an object - { props: {...} }, flat props, { redirect: {...} } or { notFound: true } - but returned null. So is a redirect whose destination cannot be sent: getServerSideProps for route "/x" returned { redirect: { destination } } that cannot be sent: redirect() URL contains control characters.
  • Headers and caching. A page that returns response headers is never cached, even with revalidate set: they are per-request (a set-cookie above all). A warning is logged. A Cache-Control you return replaces the one GioJS would send.
  • Props reach the browser. The props are serialized into the page as JSON for hydration, so never return secrets. The browser gets what JSON.stringify makes of them: a Date arrives as a string, and functions and undefined values are dropped. Props JSON cannot hold at all (a BigInt, a cycle) render the page without hydration, with a warning.
  • Without it, a page component receives { params, searchParams } (type it with PageProps<'/posts/:id'>).

Personalized renders

A cached page is served to everyone, so GioJS watches what getServerSideProps reads. Reading ctx.cookies, ctx.ip, ctx.host or ctx.scheme, or the cookie, authorization, x-forwarded-for, forwarded, x-real-ip, host, x-forwarded-host or x-forwarded-proto header (or a header an onRequest plugin changed), or enumerating ctx.headers, marks the render personal: it is not stored, even with revalidate, and a warning names the route once. The access counts, not the value - checking for a cookie that is absent still decides the page. Other headers (accept-language, user-agent) and ctx.requestId do not count. See Caching.

Types

TypeWhat it types
GetServerSidePropsGetServerSideProps<Props, Route> types the function: ctx and the result variants. Route is a pattern of your app ('/posts/:id') or a params shape ({ id: string }).
InferPagePropsInferPageProps<typeof getServerSideProps>: the props the page renders with, read off the function.
GetServerSidePropsContext, GsspContextGetServerSidePropsContext<Route> (or GsspContext<Route>, the same type): the context alone.
GetServerSidePropsResultGetServerSidePropsResult<Props>: the result union. Flat props are left out of it: a typed result always uses props.

Examples

Guard a page with redirect()

A thrown redirect() lets one helper guard pages and actions alike:

app/dashboard/page.tsx
import { redirect, type GetServerSideProps, type InferPageProps } from '@gio.js/core';
import { sessions } from '../../lib/session.server.ts';

function requireUser(ctx: { cookies: Record<string, string> }): string {
  const userId = sessions.getSession(ctx).get('userId');
  if (userId === undefined) throw redirect('/login?next=/dashboard');   // 303
  return userId;
}

export const getServerSideProps: GetServerSideProps<{ userId: string }> = async (ctx) => {
  return { props: { userId: requireUser(ctx) } };
};

export default function Dashboard({ userId }: InferPageProps<typeof getServerSideProps>) {
  return <h1>Signed in as {userId}</h1>;
}

Set cookies and other headers

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

export default function Logout() {
  return null;
}

Tag a cached render

app/posts/[id]/page.tsx
import { notFound, type GetServerSideProps, type InferPageProps } from '@gio.js/core';
import { db, type Post } from '../../../lib/db.server.ts';

export const revalidate = 3600;
export const tags = ['posts'];

export const getServerSideProps: GetServerSideProps<{ post: Post }, '/posts/:id'> = async (ctx) => {
  const post = await db.posts.find(ctx.params.id);
  if (post === null) notFound();
  // revalidateTag(`post:${id}`) purges exactly the pages that showed this post.
  return { props: { post }, tags: [`post:${post.id}`, `author:${post.authorId}`] };
};

export default function PostPage({ post }: InferPageProps<typeof getServerSideProps>) {
  return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}

Keep what the visitor typed

On the render that answers a page action, ctx.actionData holds the action's result, and ctx.method is 'POST':

app/posts/[id]/edit/page.tsx
import { notFound, redirect, type ActionArgs, type GetServerSideProps, type InferPageProps, type WithActionData } from '@gio.js/core';
import { GioForm } from '@gio.js/react';
import { db, type Post } from '../../../../lib/db.server.ts';

export async function action(req: ActionArgs<'/posts/:id/edit'>) {
  const draft = String((await req.formData()).get('body') ?? '');
  if (draft.length > 280) return { status: 422, data: { draft, error: 'At most 280 characters' } };
  await db.posts.update(req.params.id, { body: draft });
  return redirect(`/posts/${req.params.id}`);
}

export const getServerSideProps: GetServerSideProps<{ post: Post; draft: string }, '/posts/:id/edit'> = async (ctx) => {
  const post = await db.posts.find(ctx.params.id);
  if (post === null) notFound();
  const failed = ctx.actionData as { draft: string } | undefined;   // only on the POST re-render
  return { props: { post, draft: failed?.draft ?? post.body } };
};

type Props = WithActionData<typeof action, InferPageProps<typeof getServerSideProps>>;

export default function EditPost({ post, draft, actionData }: Props) {
  return (
    <GioForm>
      <h1>{post.title}</h1>
      <textarea name="body" defaultValue={draft} aria-invalid={actionData ? true : undefined} />
      {actionData && <p role="alert">{actionData.error}</p>}
      <button>Save</button>
    </GioForm>
  );
}

Good to know

  • notFound() works by throwing, so a try/catch around it swallows it: call it outside the try, or rethrow.
  • A 404, a redirect and an error page are never cached. A redirect also carries Cache-Control: private, no-cache unless its headers set one, so a CDN never stores it either.
  • redirect counts only when it is an object with a destination key, and notFound only when it is exactly true; any other value under those keys is a prop. Nest props under props to avoid surprises.
  • ctx.headers is a Proxy, so structuredClone, postMessage and worker threads reject it. Pass { ...ctx.headers }, which counts as reading every header.
  • ctx.query holds one value per name: of ?tag=a&tag=b only the last, 'b', arrives.
  • A getServerSideProps built by a module-scope call (export const getServerSideProps = withAuth(...)) is not removed from the browser bundle. Put such helpers in a *.server.ts file or import @gio.js/core/server-only in them, so a leak fails the build.
  • There is no getStaticProps: for data that changes rarely, add revalidate and the Rust cache serves the render until it is stale.

Version history

VersionChanges
v0.1.0-beta.8notFound() and { notFound: true } answer 404; returned or thrown redirect(); set-cookie arrays and headers on redirects; per-render tags; ctx.ip, ctx.scheme, ctx.host, ctx.requestId and ctx.actionData; renders that read credentials are no longer cached; typed with GetServerSideProps and InferPageProps.
v0.1.0-beta.5ctx carries method, path, headers and cookies; { props, headers } sets response headers and makes the page uncacheable; removed from browser bundles.
v0.1.0-beta.2Runs at build time under gio export.
v0.1.0-beta.1Introduced, with props and redirects.