⚠
vite-plugin-ssr has been renamed Vike, see migration guide.

Data Fetching

We recommend taking the React Tour or Vue Tour before reading this guide. The tour explains what the files /renderer/_default.page.* are about.

onBeforeRender()

The usual way to fetch data is to use a onBeforeRender() hook.

// /pages/movies.page.server.js
// Environment: server

export { onBeforeRender }

import fetch from 'node-fetch'

async function onBeforeRender(pageContext) {
  // `.page.server.js` files always run in Node.js; we could use SQL/ORM queries here.
  const response = await fetch('https://movies.example.org/api')
  let movies = await response.json()

  // `movies` will be serialized and passed to the browser; we select only the data we
  // need in order to minimize what is sent to the browser.
  movies = movies.map(({ title, release_date }) => ({ title, release_date }))

  // We make `movies` available as `pageContext.pageProps.movies`
  const pageProps = { movies }
  return {
    pageContext: {
      pageProps
    }
  }
}
// /renderer/_default.page.server.js
// Environment: server

export { render }
// Make pageContext.pageProps available in the browser
export const passToClient = ['pageProps']

import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr/server'
import { renderToHtml, createElement } from 'some-ui-framework'

async function render(pageContext) {
  const { Page, pageProps } = pageContext
  const pageHtml = await renderToHtml(
    // We pass `pageProps` to `Page`
    createElement(Page, pageProps)
  )
  /* JSX:
  const pageHtml = await renderToHtml(<Page {...pageProps} />)
  */

  return escapeInject`<html>
    <div id='view-root'>
      ${dangerouslySkipEscape(pageHtml)}
    </div>
  </html>`
}
// /renderer/_default.page.client.js
// Environment: browser

export { render }

import { hydrateDom, createElement } from 'some-ui-framework'

async function render(pageContext) {
  // Thanks to `passToClient = ['pageProps']` our `pageContext.pageProps` is
  // available here in the browser.
  const { Page, pageProps } = pageContext
  await hydrateDom(
    // We pass `pageProps` to `Page`
    createElement(Page, pageProps),
    document.getElementById('view-root')
  )
  /* JSX:
  await hydrateDom(<Page {...pageProps} />, document.getElementById('view-root'))
  */
}
// /pages/movies.page.js
// Environment: browser and server

export { Page }

// In the `render()` functions above, we pass `pageContext.pageProps` to `Page`
function Page(pageProps) {
  const { movies } = pageProps
  // ...
}

Note that vite-plugin-ssr doesn't know anything about pageProps: it's just an object we create in order to conveniently pass all the page's props at once.

Error Handling

To gracefully handle errors, use throw render() or throw redirect().

// /pages/movie/@id.page.server.js
// Environment: server

export { onBeforeRender }

import { render, redirect } from 'vite-plugin-ssr/abort'
import fetch from 'node-fetch'

async function onBeforeRender(pageContext) {
  // The @id in /pages/movie/@id.page.server.js denotes a route parameter
  const movieId = pageContext.routeParams.id
  const url = `https://movies.example.org/api/movie/${movieId}`
  const response = await fetch(url)
  if (response.status === 404) {
    // Render the error page and show a message to the user
    throw render(404, `Movie with ID ${movieId} doesn't exist.`)
    /* Or redirect the user:
    throw redirect('/movie/add')
    // Or render the movie submission form while preserving the URL:
    throw render('/movie/add')
    */
  }
  // ...
}

Upon throw render(404), the error page is rendered.

Using throw render('/movie/add') instead of throw redirect('/movie/add') is a novel technique which we explain at Guides > Authentication > Login flow.

Custom Exports

For more convenience, instead of creating a new onBeforeRender() hook for each page, we can define onBeforeRender() only once in /renderer/_default.page.server.js while using a Custom Export/Hook.

For example:

// /pages/user.page.js
export const query = { modelName: 'User', select: ['firstName', 'lastName'] }
// /pages/product.page.js
export const query = { modelName: 'Product', select: ['name', 'price'] }
// /renderer/_default.page.server.js
// Environment: server

// We use a single global `onBeforeRender()` hook
export { onBeforeRender }

import { runQuery } from 'some-sql-engine'

async function onBeforeRender(pageContext) {
  // Our `query` export values are available at `pageContext.exports.query`
  const { query } = pageContext.exports
  const data = await runQuery(query)
  const pageProps = { data }
  return { pageContext: { pageProps } }
}

Client Routing

The .page.server.js files are loaded only in Node.js; our data fetching code is always executed in Node.js. This is convenient as it makes writing data fetching code easier.

If we use Client Routing, then we have the option to define onBeforeRender() in .page.js instead of .page.server.js. In that case, onBeforeRender() is not only called in Node.js but also in the browser (upon page navigation).

In general, we recommend defining onBeforeRender() in .page.server.js (even when using Client Routing) but, if we want to minimize requests made to our Node.js server, then we can define onBeforeRender() in .page.js instead.

GraphQL

When using GraphQL, we define GraphQL queries/fragments on a component-level, while we fetch the GraphQL data in a single global onBeforeRender() hook.

In general, with vite-plugin-ssr, we have full control over rendering which means that integrating GraphQL is mostly only a matter of following the official SSR guide of the tool we want to use.

Store (Vuex/Redux...)

When using a global store (Vuex, Redux, PullState, ...), our components don't access the fetched data directly. Instead, our components only access the store, while the fetched data merely determines the initial state of the store.

In general, with vite-plugin-ssr, we have full control over rendering which means that integrating a global store is mostly only a matter of following the official SSR guide of the tool we want to use.