React Tour

Similarly to Next.js, we define a new page by creating a new .page.jsx file.

// /pages/
// Environment: Browser, Node.js

import React, { useState } from "react";
import { Counter } from "../components/Counter";

export { Page };

function Page() {
  return <>
    This page is rendered to HTML and interactive: <Counter />

By default, vite-plugin-ssr does Filesystem Routing.

FILESYSTEM                  URL
pages/        /
pages/        /about

We can also define a page's route with a Route String (for parameterized routes such as /movies/:id) or a Route Function (for full programmatic flexibility).

// /pages/
// Environment: Node.js (and Browser if we choose Client Routing)

// Note how the two files share the same base `/pages/`; this is how `vite-plugin-ssr`
// knows that `/pages/` defines the route of `/pages/`.

// Route Function
export default pageContext => pageContext.url === '/';

// If we don't create a `.page.route.js` file then vite-plugin-ssr does Filesystem Routing

Unlike Next.js, we define how our pages are rendered.

// Environment: Node.js

import ReactDOMServer from "react-dom/server";
import React from "react";
import { escapeInject, dangerouslySkipEscape } from "vite-plugin-ssr";

export { render };

async function render(pageContext) {
  const { Page, pageProps } = pageContext;
  const viewHtml = ReactDOMServer.renderToString(
    <Page {...pageProps} />

  const title = "Vite SSR";

  return escapeInject`<!DOCTYPE html>
        <div id="page-view">${dangerouslySkipEscape(viewHtml)}</div>
// Environment: Browser

import ReactDOM from "react-dom";
import React from "react";
import { getPage } from "vite-plugin-ssr/client";


async function hydrate() {
  const pageContext = await getPage(); // (`pageContext` is preloaded in production)
  const { Page, pageProps } = pageContext
    <Page {...pageProps} />,

The render() hook in gives us full control over how our pages are rendered to HTML, and gives us full control over the browser-side code. This control enables us to easily and naturally integrate any tool we want (Redux, GraphQL, Service Worker, Preact, ...).

There are four suffixes:

  • .page.js: exports the page's root React component.
  • .page.client.js: defines the page's browser-side code.
  • .page.server.js: exports page hooks (always run in Node.js).
  • .page.route.js: exports the page's Route String or Route Function.

Instead of creating a .page.client.js and .page.server.js file for each page, we can create and which apply as default for all pages.

The last two files we created are actually and, which means that we can now create a new page just by defining a new .page.jsx file (the .page.route.js file is optional).

The _default files can be overridden. For example, we can create a page with a different browser-side code than our other pages.

// /pages/

// This file is empty which means that the `/about` page has zero browser-side JavaScript.
// /pages/

export { Page };

function Page() {
  return <>This page is only rendered to HTML.<>;

By also overriding the render() hook we can even render a page with a completely different UI framework, e.g. Vue or another React version (for progressive React version upgrade).

Let's now have a look at how to fetch data.

// /pages/star-wars/
// Environment: Browser, Node.js

import React from "react";

export { Page };

function Page(pageProps) {
  const { movie } = pageProps;
  return <>
    <p>Release Date: {movie.release_date}</p>
    <p>Director: {movie.director}</p>
// /pages/star-wars/
// Environment: Node.js

// Route String
export default "/star-wars/:movieId";
// /pages/star-wars/
// Environment: Node.js

import fetch from "node-fetch";

export async function onBeforeRender(pageContext) {
  // The route parameter of `/star-wars/:movieId` is available at `pageContext.routeParams`
  const { movieId } = pageContext.routeParams;

  // `.page.server.js` files always run in Node.js; we could use SQL/ORM queries here.
  const response = await fetch(`${movieId}`);
  let movie = await response.json();

  // Our render and hydrate functions we defined earlier pass `pageContext.pageProps` to
  // the root React component `Page`; this is where we define `pageProps`.
  const pageProps = { movie };

  // We make `pageProps` available as `pageContext.pageProps`
  return {
    pageContext: {

// By default `pageContext` is available only on the server. But our hydrate function
// we defined earlier runs in the browser and needs `pageContext.pageProps`; we use
// `passToClient` to tell `vite-plugin-ssr` to serialize and make `pageContext.pageProps`
// available to the browser.
export const passToClient = ["pageProps"];

That's it for the tour and we have actually already seen most of the interface; vite-plugin-ssr is not only flexible but also simple to use.

$ npm init vite-plugin-ssr
Run $ npm init vite-plugin-ssr to scaffold a new Vite/vite-plugin-ssr app, or add vite-plugin-ssr to your existing app by following the instructions here.