@nsrv/express-jsx-views

A React and Inferno template engine for Express

Usage no npm install needed!

<script type="module">
  import nsrvExpressJsxViews from 'https://cdn.skypack.dev/@nsrv/express-jsx-views';
</script>

README

express-jsx-views

An Express template engine for JSX-based views.

Licensed under 0BSD, which basically means you can use any code here freely for any purpose, without attribution requirements. Feel free to attribute if you want, and let me know if you use my code somewhere cool!

Installation

# Required
npm i @nsrv/express-jsx-views

# For Inferno
npm i inferno inferno-hyperscript inferno-server

# For React
npm i react react-dom

The necessary babel plugins are included for both.

Usage

For development, you will want to setup nodemon or whatever restarts your server on changes to watch the view files. With nodemon, that means adding the -e option like -e js,jsx,mjs,json to include .jsx files.

Strictly speaking, you don't have to use .jsx for your view files, if you set the view engine to 'js' instead, it will work fine. But it makes a good convention, and lets you easily know which files are probably being compiled through Babel Register.

Inferno example

index.js

'use strict';

const express = require('express');
const createRenderer = require('@nsrv/express-jsx-views');

const app = express();

app.set('views', 'views');
app.set('view engine', 'jsx');
app.engine('jsx', createRenderer('inferno'));

app.get('*', (req, res) => {
    res.locals = {
        title: 'Homepage',
    };
    res.render('home', { name: 'Sam' });
});

app.listen(3000);

views/home.jsx

'use strict';

function Home({ name, _locals }) {
    return (
        <html lang="en">
            <head>
                <meta charset="UTF-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                <title>{_locals.title}</title>
            </head>
            <body>
                <h1>Hello {name}</h1>
            </body>
        </html>
    );
}

module.exports = Home;

React example

For React, just take the Inferno example and switch from

app.engine('jsx', createRenderer('inferno'));

to

app.engine('jsx', createRenderer('react'));

Also the charset prop on the first <meta> element needs to be renamed charSet for React.

Features

  • Simple React and Inferno default renderer setups.
  • Inferno default renderer passes the view props as props to the top component, but also into the global context.

You can get the global context in any function component as the second argument

// Inferno only
function MyComponent(props, context) {
  return <h1>{context._locals.title}</h1>;
}

A React equivalent is an eventually planned feature, and will probably utilize an external package exporting a React Context.

  • (Unstable) Checks that any passed view path is actually in one of the configured view folders. Unlike the Express default, you can't pass an arbitrary absolute path or traverse outside the views folders. res.render('../outside-file.jsx') will cause a throw.
    • This is for convenience and mistake catching, it's not thoroughly tested as a security measure. Don't pass arbitrary user input to res.render()'s first argument.

API

const createRenderer = require('@nsrv/express-jsx-views');

createRenderer(renderer: RendererOption, [options: Options])

RendererOption: 'inferno'|'react'|Function
// 'inferno' or 'react' will setup the basic renderers for each, with
// appropriate babel register transforms applied to the view folder
// automatically.
// A function lets you pass a custom renderer.

// A custom renderer has this signature
renderToString(Component, props)
// props will be the full Express template engine `options` object.

// A simple renderer example
const React = require('react');
const { renderToStaticMarkup } = require('react-dom/server');

function reactRenderer(Component, props) {
  return renderToStaticMarkup(React.createElement(Component, props));
}
// That's basically what the 'react' option does already. But this can be useful to add a custom element wrapper, such as a React Context Provider which you pass the props as value, to be used throughout the tree.

// Options
{
  // Universal options
  doctype: String, // default '<!DOCTYPE html>'
  // The doctype string will be prepended to all render outputs.
  // It's the only thing you simply *can't* do in JSX.
  // The default is correct (html5 and newer) for the vast majority of cases.

  // 'react' specific options
  jsxRuntime: 'automatic'|'classic', // default 'automatic'
  // By default we're using the modern JSX transform which does not require your source code to import React.
  // You can switch this to 'classic' to support older React.

  // Options when passing a custom renderer
  babelConfig: Object|Function|undefined, // default undefined
  // If you pass this option, Babel Register transform for view files will be
  // enabled, with the options given or options returned by calling a given
  // function `babelConfig(viewFolderPaths: Array)`. The viewFolderPaths
  // arguments will be `views` list Express passes to template engines.
  // example viewFolderPaths: ['/path/to/project/views']
  // The babel options will be merged with the BabelDefaults

  // 'inferno' doesn't have any extra options.
}

// BabelDefaults
{
  ignore: [],
  only: views,
  extensions: ['.jsx', '.js', '.mjs'],
  ...babelConfig, // or `...babelConfig(viewFolderPaths)`
}

// The default Inferno renderer babel config
{
    presets: [
        [
            '@babel/preset-env',
            {
                targets: {
                    node: 'current',
                },
            },
        ],
    ],
    plugins: ['babel-plugin-inferno'],
}

// The default React renderer babel config
{
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      },
    ],
    [
      '@babel/preset-react',
      {
        runtime: jsxRuntime, // default 'automatic'
      },
    ],
  ],
}

Alternatives

Advantages over express-react-views

  • Supports Inferno
  • Supports custom rendering wrappers
  • More permissive license (0BSD), no patent grant stuff

However, express-react-views might be a better choice if these things are important to you

  • Older package, longer existing stability track record
  • Clearing the require cache without server restart in development
    • express-jsx-views outsources reloading by expecting your development process to use nodemon or similar, and have it watch the .jsx files.