Data Fetching

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

onBeforeRender()

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

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

import fetch from "node-fetch";

export { onBeforeRender }

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
    }
  }
}
// _default.page.server.js
// Environment: Node.js

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

// We tell `vite-plugin-ssr` to make `pageContext.pageProps` available in the browser.
export const passToClient = ['pageProps']

export { render }

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>`
}
// _default.page.client.js
// Environment: Browser

import { getPage } from 'vite-plugin-ssr/client'
import { hydrateToDom, createElement } from 'some-view-framework'

hydrate()

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

export { Page }

// In the `render()` and `hydrate()` 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 an object we create to conveniently hold all props of the Page.

Global Logic

If we use the same data fetching logic for all/several pages we can factor out the common code.

// user.page.server.js
import { fetchData } from './fetchData'
export const onBeforeRender = pageContext => fetchData(pageContext, 'user')
// product.page.server.js
import { fetchData } from './fetchData'
export const onBeforeRender = pageContext => fetchData(pageContext, 'product')

onBeforeRender() in _default.page.server.js

Alternatively, we can define our global logic in _default.page.server.js.

// /renderer/_default.page.server.js

import { fetchData } from './fetchData'

export const onBeforeRender = pageContext => {
  const page = (
    pageContext.url.startsWith('/user') && 'user' ||
    pageContext.url.startsWith('/product') && 'product'
  )
  return fetchData(pageContext, page)
}

Multiple renderer/

If the logic differs substantially, we may want to create mutliple renderer/ directories.

onBeforeRender() in .page.server.js and _default.page.server.js.

We can also define a onBeforeRender() hook in the page's .page.server.js file, while also defining a onBeforeRender() hook in _default.page.server.js. See Multiple onBeforeRender() hooks.

onInit()

There is work-in-progress to implement a new onInit() hook to initialize global data, see #164.

Client Routing

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

That said, if we use Client Routing, then we 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 may want to define onBeforeRender() in .page.js instead.

Stateful Component

Alternatively, we can fetch data by using a stateful component.

The fetched data is then not rendered to HTML, which may defeat the reason we use SSR in the first place.

We usually need to make pageContext.routeParams available to our stateful data-fetching component:

  1. We make pageContext.routeParams available in the browser by adding it to passToClient.
  2. We make pageContext.routeParams accessible from all our components.

GraphQL

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

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.

See:

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.

See: