useClientRouter()

Overview

Environment: Browser.

To achieve Client-side Routing, we use useClientRouter() (instead of getPage()).

// *.page.client.js
// Environment: Browser

import { useClientRouter } from 'vite-plugin-ssr/client/router'

useClientRouter({
  async render(pageContext) {
    if (pageContext.isHydration) {
      // We hydrate the first page
      // ...
    } else {
      // We render a new page (when the user navigates)
      // ...
    }
  }
})

See also: What is Hydration?

Examples

React example:

Vue example:

Usage & options

// _default.page.client.js
// Environment: Browser

import { useClientRouter } from 'vite-plugin-ssr/client/router'
import { render, hydrate } from 'some-ui-framework'

const { hydrationPromise } = useClientRouter({
  async render(pageContext) {
    // `pageContext.isHydration` is set by `vite-plugin-ssr` and is `true` when the page
    // is already rendered to HTML.
    if (pageContext.isHydration) {
      // We hydrate the first page. (Since we do SSR, the first page is already
      // rendered to HTML and we merely have to hydrate it.)
      await hydrate(pageContext.Page)
    } else {
      // We render a new page. (When the user navigates to a new page.)
      await render(pageContext.Page)
    }
  },

  // If `ensureHydration: true` then `vite-plugin-ssr` ensures that the first render is always
  // a hydration. (In other words, the hydration process is never interrupted — even if the
  // user clicks on a link before the hydration started. Default value: `false`.)
  // If we use Vue, we need `ensureHydration: true` to avoid "Hydration Mismatch" errors.
  // If we use React, we can leave `ensureHydration: false` for a slight performance improvement.
  ensureHydration: true,

  // See `Link prefetching` section below. Default value: `false`.
  prefetchLinks: true,

  // To create custom page transition animations
  onTransitionStart,
  onTransitionEnd
})

hydrationPromise.then(() => {
  console.log('Hydration finished; page is now interactive.')
})

function onTransitionStart() {
  console.log('Page transition start')
  // For example:
  document.body.classList.add('page-transition')
}
function onTransitionEnd() {
  console.log('Page transition end')
  // For example:
  document.body.classList.remove('page-transition')
}

Note that pageContext is completely discarded and created anew upon page navigation. (That's why the context object is called pageContext — not appContext.)

We can keep using <a href="/some-url"> links: the Client Router automatically intercepts clicks on <a> elements.

We can skip the Client Router by adding the rel="external" attribute, e.g. <a rel="external" href="/some/url">The Client Router won't intercept me</a>.

We can use navigate('/some/url') to programmatically navigate our user to a new page.

By default, the Client-side Router scrolls to the top of the page upon page change; we can use <a keep-scroll-position /> / navigate('/some/url', { keepScrollPosition: true }) if we want to preserve the scroll position instead. (Useful for Nested Routes.)

State initialization

Usually, when using tools such as Apollo GraphQL, Redux or Vuex, we determine the initial state of our UI on the server-side while rendering HTML, and then initialize the client-side with that initial state.

Depending on the tool, we do either one of the following:

  • We initialize the state once.
  • We re-initialize the state on every page navigation.

To initialize once:

// _default.page.server.js
// Environment: Node.js

export { render }
export { passToClient }

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

const passToClient = ['initialState']

// The `render()` hook is called only for the first page.
// (Whereas `onBeforeRender()` is called as well upon page navigation.)
async function render(pageContext) {
  const initialState = await getInitialState()

  // We use `initialState` for rendering the HTML, so that the HTML contains
  // the content of `initialState`.
  const pageHtml = await renderToHtml(pageContext.Page, initialState)

  const documentHtml = escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div>${dangerouslySkipEscape(pageHtml)}</div>
      </body>
    </html>`

  return {
    documentHtml,
    pageContext: {
      initialState
    }
  }
}
// _default.page.client.js
// Environment: Browser

import { useClientRouter } from 'vite-plugin-ssr/client/router'
import { initClientSide } from './initClientSide'

useClientRouter({
  ensureHydration: true,
  async render(pageContext) {
    // The first page is rendered to HTML and `pageContext.isHydration === true`
    if (pageContext.isHydration) {
      // `pageContext.initialState` is available here
      initClientSide(pageContext.initialState)
      // If we don't set `ensureHydration: true`, then this if-block may never
      // be called. (The first page rendering and the hydration may otherwise
      // be interrupted as explained in the `Usage & Options` section.)
    } else {
      // Note that `pageContext.initialState` is not available here,
      // since our `render()` hook is only called for the first page.
    }

    // ...
  }
})

To initialize on every page navigation:

// _default.page.server.js
// Environment: Node.js

export { onBeforeRender }
export { passToClient }

import { getInitialState } from './getInitialState'

const passToClient = ['initialState']

// The `onBeforeRender()` hook is called for the first page as well as upon page navigation.
// (Whereas `render()` is called only for the first page.)
async function onBeforeRender() {
  const initialState = await getInitialState()
  return {
    pageContext: {
      initialState
    }
  }
}
// _default.page.client.js
// Environment: Browser

import { useClientRouter } from 'vite-plugin-ssr/client/router'
import { initClientSide } from './initClientSide'

useClientRouter({
  async render(pageContext) {
    // We initialize the state for every page rendering. So not only
    // the first page but also any subsequent page navigation.
    initClientSide(pageContext.initialState)

    // ...
  }
})

By default, when the user hovers his mouse over a link <a href="/some-url">, the static assets of the page /some-url are loaded. This means that static assets are often already loaded before even the user clicks on the link.

We can prefetch even more eagerly by setting useClientRouter({ prefetchLinks: true }): the links are then prefetched as soon as they appear in the user's browser viewport.

We can override the link prefetching behavior for individual links by setting <a data-prefetch="false" href="/some-url" />.

Only the page's static assets are prefetched; the page's pageContext is currently not prefetched but it's a work-in-progress feature, see #246.