DEV Community

Roeland
Roeland

Posted on • Updated on

Generate HTML on the server with Deno and JSX

When you want to generate html on the server you will need some template engine.
You can use, EJS, handlebars or many others, but I prefer a template engine where I can create components instead of working with partials and layouts.

Luckily Deno has built-in support for JSX thanks to swc. JSX is a syntax extension to Javascript. This means that JSX will be translated to real javascript calls like React.createElement() (more on this below). This is nice, but Deno doesn't know about React so we need to do some more work.

First we will create a simple Deno application to get started.
Put this code in main.js:

import { serve } from "https://deno.land/std@0.92.0/http/server.ts";

const server = serve({ port: 8000 });
const headers = new Headers();
headers.append("Content-Type", "text/html; charset=UTF-8");

for await (const req of server) {
  req.respond({
    status: 200,
    headers,
    body: `
      <!DOCTYPE html>
      <html>
        <head>
          <title>Hello</title>
          <link 
            href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" 
            rel="stylesheet">
        </head>
        <body>
          <h1 class="text-3xl m-2">Hello world</h1>
          <button 
            class="border bg-indigo-600 text-white px-2 py-1 rounded m-2">
            Useless button
          </button>
        </body>
      </html>`,
  });
}
Enter fullscreen mode Exit fullscreen mode

You can start this with deno run --allow-net ./main.js. Now you can open your browser on localhost:8000 and view the html page.

The goal is to replace this html template string with JSX components.

React 16

It's time to create our first component pages/home.jsx.
For now this is a single component with the complete html.

import React from "https://jspm.dev/react@16.14.0";

export default function () {
  return (
    <html>
      <head>
        <title>Hello</title>
        <link
          href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"
          rel="stylesheet"
        />
      </head>
      <body>
        <h1 className="text-3xl m-2">Hello world</h1>
        <button
          className="border bg-indigo-600 text-white px-2 py-1 rounded m-2"
        >
          Useless button
        </button>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now it's time to use this component in main.js

import { serve } from "https://deno.land/std@0.92.0/http/server.ts";
import ReactDOMServer from 'https://jspm.dev/react-dom@16.14.0/server';
import home from "./pages/home.jsx"

function render(jsx) {
  return ReactDOMServer.renderToString(jsx());
}

const server = serve({ port: 8000 });
const headers = new Headers();
headers.append("Content-Type", "text/html; charset=UTF-8");

for await (const req of server) {
  req.respond({
    status: 200,
    headers: headers,
    body: render(home),
  });
}
Enter fullscreen mode Exit fullscreen mode

This includes a new render function that executes the JSX function and renders the result to a string.
Instead of renderToString you can also use renderToStaticMarkup.

We now have a working JSX example with React 16!

React 17 is at the moment of writing not yet supported. The problem is that in React 17 the JSX is translated to something new. It isn't React.createElement anymore to avoid the need for importing React.

I first tried to load React 16 from Skypack CDN, but that doesn't work because of this issue

Preact

It is also possible to use Preact instead of React.

Since JSX is translated to React.createElement() we have to replace React with some other class.
There are 2 ways of doing this.

  • with a JSX pragma
  • with tsconfig.json
 /** @jsx h */
import { h } from "https://cdn.skypack.dev/preact";
Enter fullscreen mode Exit fullscreen mode

The first line is the JSX pragma. This means to use h instead of React.
But you can also use tsconfig.json so you don't need the pragma everywhere.
You have to run Deno with the link to the config deno run --config ./tsconfig.json ...

{
    "compilerOptions": {
        "jsx": "react",
        "jsxFactory": "h",
    }
}
Enter fullscreen mode Exit fullscreen mode

The render function in main.js looks like this:

import renderToString from "https://cdn.skypack.dev/preact-render-to-string@v5.1.12";

function render(jsx) {
  return renderToString(jsx());
}
Enter fullscreen mode Exit fullscreen mode

Result

In the final version I created extra components for the layout and for the button.

pages/home.jsx

import React from "https://jspm.dev/react@16.14.0";
import Layout from "../components/layout.jsx";
import Button from "../components/button.jsx";

export default function () {
  return (
    <Layout title="Hello">
      <h1 className="text-3xl m-2">Hello world</h1>
      <Button>
        Useless button
      </Button>
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

components/button.jsx

import React from "https://jspm.dev/react@16.14.0";

export default function ({ children }) {
  return (<button
    className="border bg-indigo-600 text-white px-2 py-1 rounded m-2"
  >
    {children}
  </button>);
}
Enter fullscreen mode Exit fullscreen mode

components/layout.jsx

import React from "https://jspm.dev/react@16.14.0";

export default function ({ children, title }) {
  return (
    <html>
      <head>
        <title>{title}</title>
        <link
          href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"
          rel="stylesheet"
        />
      </head>
      <body>
        {children}
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

I hope this will get you started with JSX in Deno. This is just a simple example and there is a lot to improve like using deps.ts and Typescript. But I try to keep this example focussed on JSX.

You can find all the code here.

Top comments (0)