⚠️ The
vite-plugin-ssr
project has been renamed
Vike.
- If you are already using vite-plugin-ssr then migrate to Vike.
- For new projects, don't use vite-plugin-ssr but use Vike instead.
clientRouting
Environment: browser.
By default, vite-plugin-ssr
does Server Routing.
We opt into Client Routing by modifying /renderer/_default.client.js
to:
- Set
export const clientRouting = true
.
- Update and adapt the client-side
render()
hook to
hydrate the DOM only when rendering the first page.
Instead of manually integrating Client Routing yourself, you can use vike-*
packages and Bati: vike-*
packages use Client Routing and Bati helps you scaffold projects. Bati and vike-*
packages are going to get out of beta soon.
React example:
Vue example:
Usage & options
// /renderer/_default.page.client.js
// Environment: browser
export { render }
// Enable Client Routing
export const clientRouting = true
// See "Link prefetching" section below. Default value: 'hover'.
export const prefetchStaticAssets = 'viewport'
// Whether your UI framework allows the hydration to be aborted. (Allowing vite-plugin-ssr
// to abort the hydration if the user clicks on a link before the hydration finished.)
// React users should set hydrationCanBeAborted to true. (Other frameworks,
// such as Vue, crash if the hydration is aborted.)
export const hydrationCanBeAborted = true
// Create custom page transition animations
export { onPageTransitionStart }
export { onPageTransitionEnd }
// The onHydrationEnd() hook is called after the render() hook finishes for the
// first page the user navigates to (the onHydrationEnd() hook isn't called for
// subsequent page navigation).
export { onHydrationEnd }
the user navigates to the render() hook the first page finishes rendering render())
import { renderToDom, hydrateDom } from 'some-ui-framework'
async function 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 renderToDom(pageContext.Page)
}
}
}
function onPageTransitionStart(pageContext) {
console.log('Page transition start')
// `pageContext.isBackwardNavigation` is also set at `render(pageContext)`
// and `onPageTransitionEnd(pageContext)`.
console.log('Is backwards navigation?', pageContext.isBackwardNavigation)
// For example:
document.body.classList.add('page-transition')
}
function onPageTransitionEnd(pageContext) {
console.log('Page transition end')
// For example:
document.body.classList.remove('page-transition')
}
function onHydrationEnd() {
console.log('The page is now interactive')
}
Note that pageContext
is completely discarded and created anew upon page navigation.
That's why it's called pageContext (and 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 the 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 href="/some/url" keep-scroll-position />
/ navigate('/some/url', { keepScrollPosition: true })
if we want to preserve the scroll position instead. (Useful for Nested Layouts.)
See Render Modes for another illustration of conditional DOM hydration.
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.
Reading Recommendation.
To initialize once:
// /renderer/_default.page.server.js
// Environment: server
export { render }
export { passToClient }
import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr/server'
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
}
}
}
// /renderer/_default.page.client.js
// Environment: browser
export { render }
import { initClientSide } from './initClientSide'
async function render(pageContext) {
// The first page is rendered to HTML and `pageContext.isHydration === true`
if (pageContext.isHydration) {
// `pageContext.initialState` is available here
initClientSide(pageContext.initialState)
} else {
// Note that `pageContext.initialState` isn't available here,
// since our `render()` hook is only called for the first page.
}
// ...
}
To initialize on every page navigation:
// /renderer/_default.page.server.js
// Environment: server
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
}
}
}
// /renderer/_default.page.client.js
// Environment: browser
export { render }
import { initClientSide } from './initClientSide'
async function 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)
// ...
}
Link prefetching
By default,
the static assets of /some-url
are loaded as soon as the user hovers his mouse over a link <a href="/some-url">
.
This means that static assets are often already loaded before even the user clicks on the link.
We can prefetch even more eagerly by using viewport prefetching: the links are then prefetched as soon as they appear in the viewport of the user's browser.
// /renderer/_default.page.client.js
// Environment: browser
// Prefetch links as soon as they enter the viewport
export const prefetchStaticAssets = 'viewport'
// Prefetch links when the user's mouse hovers a link
export const prefetchStaticAssets = 'hover'
// Disable prefetching
export const prefetchStaticAssets = false
Viewport prefetching is disabled in development. (Because it would make Vite transpile all preloaded pages and thus siginficantly slow down development.)
To override the setting for individual links: <a data-prefetch-static-assets="hover" href="/some-url" />
(or data-prefetch-static-assets="false"
and data-prefetch-static-assets="viewport"
).
We can programmatically prefetch('/some/url')
.
Only static assets are prefetched: the pageContext
of pages isn't prefetched, see #246.
We can viewport prefetch for mobile users while only hover prefetch for desktop users:
// For small screens, such as mobile, viewport prefetching can be a sensible strategy
export const prefetchStaticAssets = window.matchMedia('(max-width: 600px)').matches
? 'viewport' : 'hover'
// Or we enable viewport prefetching for any device without a mouse: mobile and tablets (but
// not laptops that have a touch display).
export const prefetchStaticAssets = window.matchMedia('(any-hover: none)').matches
? 'viewport' : 'hover'
See also: