Skip to content

Use Vite for server side rendering in Node

License

Notifications You must be signed in to change notification settings

tfxterry/vite-ssr

 
 

Repository files navigation

Vite SSR logo

Vite SSR

Simple yet powerlful Server Side Rendering for Vite 2 in Node.js.

  • ⚡ Lightning Fast HMR (powered by Vite, even in SSR mode).
  • 💁‍♂️ Consistent DX experience abstracting most of the SSR complexity.
  • 🔍 Small library, unopinionated about your page routing and API logic.
  • 🔥 Fast and SEO friendly thanks to SSR, with SPA takeover for snappy UX.
  • 🧱 Compatible with Vite's plugin ecosystem such as file-based routing, PWA, etc.

Start a new SSR project right away using Vue, filesystem routes, page layouts, icons auto-import and more with Vitesse SSR template. See live demo.

Vite SSR can be deployed to any Node.js environment, including serverless platforms like Vercel or Netlify. It can create pages dynamically from a cloud function and cache the result at the edge network for subsequent requests, effectively behaving as statically generated pages with no cost.

See Vitedge for SSR in Cloudflare Workers.

Installation

Create a normal Vite project for Vue or React.

yarn create @vitejs/app my-app --template [vue|vue-ts|react|react-ts]

Then, add vite-ssr with your package manager (direct dependency) and your framework router.

# For Vue
yarn add vite-ssr vue@3 vue-router@4 @vueuse/head

# For React
yarn add vite-ssr react@16 react-router-dom@5

Usage

Add Vite SSR plugin to your Vite config file (see vite.config.js for a full example).

// vite.config.js
import vue from '@vitejs/plugin-vue'
import viteSSR from 'vite-ssr/plugin.js'
// import reactRefresh from '@vitejs/plugin-react-refresh'

export default {
  plugins: [
    viteSSR(),
    vue(), // reactRefresh()
  ],
}

Then, simply import the main Vite SSR handler in your main entry file as follows. See full examples for Vue and React.

import App from './App' // Vue or React main app
import routes from './routes'
import viteSSR from 'vite-ssr'

export default viteSSR(App, { routes }, (context) => {
  /* custom logic */
  /* const { app, router, initialState, ... } = context */
})

That's right, in Vite SSR there's only 1 single entry file by default 🎉. It will take care of providing your code with the right environment.

If you need conditional logic that should only run in either client or server, use Vite's import.meta.env.SSR boolean variable and the tree-shaking will do the rest.

Available options

The previous handler accepts the following options as its second argument:

  • routes: Array of routes, according to each framework's router (see vue-router or react-router-config).
  • base: Function that returns a string with the router base. Can be useful for i18n routes or when the app is not deployed at the domain root.
  • pageProps.passToPage: Whether each route's initialState should be automatically passed to the page components as props.
  • debug.mount: Pass false to prevent mounting the app in the client. You will need to do this manually on your own but it's useful to see differences between SSR and hydration.

Using separate entry files

Even though Vite SSR uses 1 single entry file by default, thus abstracting complexity from your app, you can still have separate entry files for client and server if you need more flexibility. This can happen when building a library on top of Vite SSR, for example.

Simply provide the entry file for the client in index.html (as you would normally do in an SPA) and pass the entry file for the server as a CLI flag: vite-ssr [dev|build] --ssr <path/to/entry-server>.

Then, import the main SSR handlers for the entry files from vite-ssr/vue/entry-client and vite-ssr/vue/entry-server instead. Use vite-ssr/react/* for React.

SSR initial state and data fetching

The SSR initial state is the application data that is serialized as part of the server-rendered HTML for later hydration in the browser. This data is normally gathered using fetch or DB requests from your API code.

Vite SSR initial state consists of a plain JS object that is passed to your application and can be modified at will during SSR. This object will be serialized and later hydrated automatically in the browser, and passed to your app again so you can use it as a data source.

export default viteSSR(App, { routes }, ({ initialState }) => {
  if (import.meta.env.SSR) {
    // Write in server
    initialState.myData = 'DB/API data'
  } else {
    // Read in browser
    console.log(initialState.myData) // => 'DB/API data'
  }

  // Provide the initial state to your stores, components, etc. as you prefer.
})
Initial state in Vue

Vue has multiple ways to provide the initial state to Vite SSR:

  • Calling your API before entering a route (Router's beforeEach or beforeEnter) and populate route.meta.state. Vite SSR will get the first route's state and use it as the SSR initial state. See a full example here.
export default viteSSR(App, { routes }, async ({ app }) => {
  router.beforEach((to, from, next) => {
    if (to.meta.state) {
      return next() // Already has state
    }

    const response = await fetch('my/api/data')

    // This will modify initialState
    to.meta.state = await response.json()

    next()
  })
})
  • Calling your API directly from Vue components and save the result in the SSR initial state. You can rely on Vue's serverPrefetch or suspense to await for your data and then render the view. See a full example with suspense here.
// Main
export default viteSSR(App, { routes }, ({ app, initialState }) => {
  // You can pass it to your state management, if you like that
  const store = createStore({ state: initialState /* ... */ })
  app.use(store)

  // Or provide it to child components
  app.provide('initial-state', initialState)
})

// Page Component with Server Prefetch
export default {
  async serverPrefetch() {
    await this.fetchMyData()
  },
  async beforeMount() {
    await this.fetchMyData()
  },
  methods: {
    async fetchMyData() {
      const data = await (await fetch('my/api/data')).json()
      const store = useStore()
      store.commit('myData', data)
    },
  },
}
// Page Component with Async Setup
export default {
  async setup() {
    const data = await (await fetch('my/api/data')).json()
    const store = useStore()
    store.commit('myData', data)

    return { data }
  },
}

// Use Suspense in your app root
<template>
  <RouterView v-slot="{ Component }">
    <Suspense>
      <component :is="Component" />
    </Suspense>
  </RouterView>
</template>

Initial state in React

There are a few ways to provide initial state in React:

  • Call your API and throw a promise in order to leverage React's Suspense (in both browser and server) anywhere in your components. Vite SSR is already adding Suspense to the root so you don't need to provide it.
function App({ initialState }) {
  if (!initialState.ready) {
    const promise = getPageProps(route).then((state) => {
      Object.assign(initialState, state)
      initialState.ready = true
    })

    // Throw the promise so Suspense can await it
    throw promise
  }

  return <div>{initialState}</div>
}
  • Calling your API before entering a route and populate route.meta.state. Vite SSR will get the first route's state and use it as the SSR initial state. See a full example here.
function App({ router }) {
  // This router is provided by Vite SSR.
  // Use it to render routes and save initial state.

  return (
    <Switch>
      {router.routes.map((route) => {
        if (!route.meta.state) {
          // Call custom API and return a promise
          const promise = getPageProps(route).then((state) => {
            // This is similar to modifying initialState in the previous example
            route.meta.state = state
          })

          // Throw the promise so Suspense can await it
          throw promise
        }

        return (
          <Route key={route.path} path={route.path}>
            <route.component props={...route.meta.state} />
          </Route>
        )
      })}
    </Switch>
  )
}

State serialization

Vite SSR simply uses JSON.stringify to serialize the state and saves it in the DOM. This behavior can be overriden by using the transformState hook:

import viteSSR from 'vite-ssr'
import App from './app'
import routes from './routes'

export default viteSSR(App, {
  routes,
  transformState(state) {
    if (import.meta.env.SSR) {
      // Serialize during SSR by using,
      // for example, @nuxt/devalue
      return customSerialize(state)
    } else {
      // Deserialize in browser
      return customDeserialize(state)
    }
  },
})

Head tags and global attributes

Use your framework's utilities to handle head tags and attributes for html and body elements.

Vue Head

Install @vueuse/head as follows:

import { createHead } from '@vueuse/head'

export default viteSSR(App, { routes }, ({ app }) => {
  const head = createHead()
  app.use(head)

  return { head }
})

// In your components:
// import { useHead } from '@vueuse/head'
// ... useHead({ ... })

React Helmet

Use react-helmet-async from your components (similar usage to react-helmet). The provider is already added by Vite SSR.

import { Helmet } from 'react-helmet-async'

// ...
;<>
  <Helmet>
    <html lang="en" />
    <meta charSet="utf-8" />
    <title>Home</title>
    <link rel="canonical" href="http://mysite.com/example" />
  </Helmet>
</>

Rendering only in client/browser

Vite SSR exports ClientOnly component that renders its children only in the browser:

import { ClientOnly } from 'vite-ssr'

//...
;<div>
  <ClientOnly>
    <div>...</div>
  </ClientOnly>
</div>

Development

There are two ways to run the app locally for development:

  • SPA mode: vite dev command runs Vite directly without any SSR.
  • SSR mode: vite-ssr dev command spins up a local SSR server. It supports similar attributes to Vite CLI, e.g. vite-ssr --port 1337 --open.

SPA mode will be slightly faster but the SSR one will have closer behavior to a production environment.

Production

Run vite-ssr build for buildling your app. This will create 2 builds (client and server) that you can import and use from your Node backend. See an Express.js example server here, or a serverless function deployed to Vercel here.

References

The following projects served as learning material to develop this tool:

Todos

  • TypeScript
  • Make src/main.js file name configurable
  • Support build options as CLI flags (--ssr entry-file supported)
  • Support React
  • SSR dev-server
  • Make SSR dev-server similar to Vite's dev-server (options, terminal output)
  • Research if vite-ssr CLI logic can be moved to the plugin in Vite 2 to use vite command instead.
  • Docs

About

Use Vite for server side rendering in Node

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 97.4%
  • JavaScript 2.6%