Search code examples
node.jsserver-side-renderingweb-componentlit-elementlit

LitElement and server side rendering


I've been trying to make a Web Component with LitElement that works with server side rendering environments (SSR) like NextJS and VueJS, without the need for any janky client-side-only imports. According to Lit's documentation this should be possible, but I keep running into window is not defined in NextJS.

What I've got so far is this:

import { render } from '@lit-labs/ssr'
import { TestComponent } from './components/test-component'

const ssrResult = render(TestComponent)

export default ssrResult

Apart from adding a couple of hundred kB to the build the @lit-labs/ssr-render function seems to do nothing. What I'm trying to achieve, by the way, is not necessarily actual SSR – I'm happy to use any workaround, as long as it's on my end.

The component is structured like this:

import {
  html,
  LitElement,
  TemplateResult
} from 'lit'
import {
  customElement,
  property,
} from 'lit/decorators.js'

@customElement('test-component')
export class TestComponent extends LitElement {

  @property({ type: String })
  public title = ''

  render(): TemplateResult | void {
    return html`
      <div class="content">
        <h3>${this.title}</h3>
      </div>
    `
  }
}

Here's my rollup config:

import { babel } from '@rollup/plugin-babel'
import commonjs from '@rollup/plugin-commonjs'
import filesize from 'rollup-plugin-filesize'
import nodePolyfills from 'rollup-plugin-polyfill-node'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import terser from '@rollup/plugin-terser'
import typescript from '@rollup/plugin-typescript'

import pkg from './package.json' assert { type: 'json' }

//Node hack
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
global['__filename'] = __filename

const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs']

export default {
  input: './src/index.ts',
  output: [
    {
      file: pkg.main,
      format: 'umd',
      name: pkg.name
    },
    {
      file: pkg.module,
      format: 'es'
    }
  ],
  plugins: [
    nodePolyfills(),
    nodeResolve({
      extensions,
      jsnext: true,
      module: true,
    }),
    commonjs(),
    typescript({
      tsconfig: './tsconfig.json'
    }),
    babel({
      extensions,
      exclude: ['./node_modules/**'],
      babelHelpers: 'bundled'
    }),
    filesize(),
    terser(),
  ],
}

Solution

  • As of [email protected] (https://github.com/lit/lit/releases/tag/lit%402.3.0) it is possible to directly import Lit components into a Next.js project without any client only import workarounds. You do not need to use @lit-labs/ssr package for that.

    So something like below is fine.

    // pages/index.tsx
    
    import '../components/test-component';
    
    export default function Home() {
      return <test-component />
    }
    

    It does not deeply render the custom element's content though, so you'll just get the host custom element tag in the server rendered HTML, though since you mention you're not looking for "actual SSR", perhaps this is all you need.

    Once client JS is loaded, the custom element will be upgraded and its contents will render.

    You can see an example project here https://github.com/lit/lit/tree/0b35d83ef4f9a72e6520e7a065353c49d44974ac/examples/nextjs

    As a side note based on the property you have on your test component, it is best to avoid any global attributes like title as a property specifier. https://web.dev/custom-elements-best-practices/#attributes-and-properties