getServerSideProps
Load a page's data on the server for each render, and answer with props, a redirect, a 404 or response headers.
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:
| Field | Type | Default | Description |
|---|---|---|---|
method | string | - | 'GET' or 'HEAD', or 'POST' on the render that answers a page action. |
path | string | - | The routed path, without the query string or a locale prefix (after [[rewrites]]). |
params | ParamsOf<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 ''. |
query | Record<string, string> | - | The query string, one value per name: the last one when a name repeats. |
headers | Record<string, string> | - | The request headers, names lowercase. A tracked view (a Proxy): see personalized renders. |
cookies | Record<string, string> | - | The Cookie header, parsed. Reading it makes the render personal. |
locale | string | undefined | - | The request locale with [i18n] configured; absent without it. |
ip | string | undefined | - | The client's address, proxy-aware ([server] trusted_proxies). Reading it makes the render personal. |
scheme | string | undefined | - | 'https' or 'http', as the client used it. Reading it makes the render personal. |
host | string | undefined | - | The host the client addressed, which is whatever the client sent unless a proxy pins it. Reading it makes the render personal. |
requestId | string | undefined | - | This request's X-Request-Id, also on every log line. Reading it does not make the render personal. |
actionData | unknown | - | The page action's result, on the render that answers a POST; absent on every GET. |
Returns
Return (or resolve to) one of these:
| Value | Answer |
|---|---|
{ props, headers?, tags? } | 200 with the page rendered from props. headers are response headers; tags are cache tags for this render. |
| Any other object | Used 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. Undergio exportit runs once per exported page, at build time, with emptyheaders,cookiesandquery. - Thrown answers.
notFound()andredirect()may be thrown fromgetServerSidePropsor anything it calls; they answer like the returned forms. Any other error answers500with the nearesterror.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 whosedestinationcannot 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
revalidateset: they are per-request (aset-cookieabove all). A warning is logged. ACache-Controlyou 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.stringifymakes of them: aDatearrives as a string, and functions andundefinedvalues are dropped. Props JSON cannot hold at all (aBigInt, a cycle) render the page without hydration, with a warning. - Without it, a page component receives
{ params, searchParams }(type it withPageProps<'/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
| Type | What it types |
|---|---|
GetServerSideProps | GetServerSideProps<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 }). |
InferPageProps | InferPageProps<typeof getServerSideProps>: the props the page renders with, read off the function. |
GetServerSidePropsContext, GsspContext | GetServerSidePropsContext<Route> (or GsspContext<Route>, the same type): the context alone. |
GetServerSidePropsResult | GetServerSidePropsResult<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:
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
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
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':
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 atry/catcharound it swallows it: call it outside thetry, or rethrow.- A 404, a redirect and an error page are never cached. A redirect also carries
Cache-Control: private, no-cacheunless its headers set one, so a CDN never stores it either. redirectcounts only when it is an object with adestinationkey, andnotFoundonly when it is exactlytrue; any other value under those keys is a prop. Nest props underpropsto avoid surprises.ctx.headersis aProxy, sostructuredClone,postMessageand worker threads reject it. Pass{ ...ctx.headers }, which counts as reading every header.ctx.queryholds one value per name: of?tag=a&tag=bonly the last,'b', arrives.- A
getServerSidePropsbuilt by a module-scope call (export const getServerSideProps = withAuth(...)) is not removed from the browser bundle. Put such helpers in a*.server.tsfile or import@gio.js/core/server-onlyin them, so a leak fails the build. - There is no
getStaticProps: for data that changes rarely, addrevalidateand the Rust cache serves the render until it is stale.
Related
- Fetching Data - the guide
- Caching & Revalidating
revalidate,tags,shellgenerateMetadata- receives the same context and the propsactionredirect,notFoundserver-only
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | notFound() 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.5 | ctx carries method, path, headers and cookies; { props, headers } sets response headers and makes the page uncacheable; removed from browser bundles. |
v0.1.0-beta.2 | Runs at build time under gio export. |
v0.1.0-beta.1 | Introduced, with props and redirects. |