@riotjs/ssr

Riot module to render riot components on the server

Usage no npm install needed!

<script type="module">
  import riotjsSsr from 'https://cdn.skypack.dev/@riotjs/ssr';
</script>

README

ssr

Riot module to render riot components on the server

Build Status

NPM version NPM downloads MIT License

Installation

npm i -S riot @riotjs/compiler @riotjs/ssr

Usage

render - to render only markup

You can simply render your components markup as it follows:

import MyComponent from './my-component.riot'
import render from '@riotjs/ssr'

const html = render('my-component', MyComponent, { some: 'initial props' })

Notice that components rendered on the server will always automatically receive the isServer=true property.

renderAsync - to handle asynchronous rendering

Components that can not be rendered synchronously must expose the onAsyncRendering method to the renderAsync function. For example:

<async-component>
  <p>{ state.username }<p>

  <script>
    export default {
      onBeforeMount({ isServer }) {
        // if it's not SSR we load the user data right the way
        if (!isServer) {
          this.loadUser()
        }
      },
      loadUser() {
        return fetch('/user/name').then(({name}) => {
          this.update({ name })
        })
      },
      // this function will be automatically called only
      // if the component is rendered via `renderAsync`
      onAsyncRendering() {
        return this.loadUser()
      }
    }
  </script>
</async-component>

The above component can be rendered on the server as it follows:

import MyComponent from './async-component.riot'
import {renderAsync} from '@riotjs/ssr'

renderAsync('async-component', MyComponent, { some: 'initial props' }).then(html => {
  console.log(html)
})

Notice that the onAsyncRendering can either return a promise or use the resolve, reject callbacks:

export default {
  // this is ok
  async onAsyncRendering() {
    await loadData()
  }
}
export default {
  // this is also ok
  onAsyncRendering(resolve, reject) {
    setTimeout(resolve, 1000)
  }
}

IMPORTANT nested onAsyncRendering on children components are not supported!

fragments - to render html and css

You can also extract the rendered html and css separately using the fragments function:

import MyComponent from './my-component.riot'
import {fragments} from '@riotjs/ssr'

const {html, css} = fragments('my-component', MyComponent, { some: 'initial props' })

renderAsyncFragments - to handle asynchronous fragments rendering

It works like the method above but asynchronously

register - to load riot components in node

If you only want load your components source code in a node environement you just need to register the riot loader hook:

import register from '@riotjs/ssr/register'

import MyComponent from './my-component.riot' // It will fail

// from now on you can load `.riot` tags in node
const unregister = register()

import MyComponent from './my-component.riot' // it works!

// normally you will not need to call this function but if you want you can unhook the riot loader
unregister()

Advanced tips

If you want to render your whole document you can simply pass html as name of your root node. For example

<html>
    <head>
        <title>{ state.message }</title>
        <meta each={ meta in state.meta } {...meta}/>
    </head>

    <body>
        <p>{ state.message }</p>
        <script src='path/to/a/script.js'></script>
    </body>

    <script>
        export default {
          state: {
            message: 'hello',
            meta: [{
              name: 'description',
              content: 'a description'
            }]
          }
        }
    </script>
</html>

It can be rendered as it follows:

import MyRootApplication from './my-root-application.riot'
import render from '@riotjs/ssr'

const html = render('html', MyRootApplication) 

Better SSR control using the createRenderer

For a better control over your HTML rendering you might want to use the createRenderer factory function. This method allows the creation of a rendering function receiving the {getHTML, css, dispose, element} option object.

  • getHTML: give you the rendered html of your component as string
  • css: the css of your component as string
  • dispose: clean the memory used on the server needed to render your component
  • element: the component instance you are mounting

For example

import MyComponent from './my-component.riot'
import { createRenderer } from '@riotjs/ssr'

const logRendrer = createRenderer(({getHTML, css, dispose, component}) => {
  const html = getHTML()
  
  console.log('Rendering the component: %s', component.name)
  
  dispose()
  return { html, css }
})

// use your logRenderer
const { html, css } = logRendrer('my-component', MyComponent, { some: 'initial props' })

DOM Globals

@riotjs/ssr needs DOM globals (like window, document ...) to properly render your markup. With the domGlobals exported object you can decide manually when the globals should be created and deleted from in your node applications.

import { domGlobals } from '@riotjs/ssr'

domGlobals.create()

// global DOM object in your node environement are now defined 
console.log(global.window, global.document) 

// they will be cleared and will be undefined
domGlobals.clear()

Caveat

If you are rendering your whole HTML you will not be able to use multiple times the inline <script> <style> tags. Of course you can use only once the ones used by Riot.js to customize your components. For example:

<html>
    <head>
        <!-- allowed -->
        <script src='path/to/some/script.js'></script>

        <!-- not allowed -->
        <style>
        </style>

        <!-- not allowed -->
        <script>
            const globalstuff = {}
        </script>
    </head>

    <body>
        <!-- application html -->
    </body>

    <!-- allowed -->
    <script>
        export default {
            // app code
        }
    </script>
    
    <!-- allowed -->
    <style>
        :host {}
    </style>
</html>