React Build-less MVP

Going Build-less

I don’t do a huge amount of front end work, but from time to time I do need to put together a small front-end.

Front-end work is some of the most complicated tech work there is. Not only do you need to know HTML, CSS and JavaScript, but you need to know how to configure and use build tools, npm, frameworks, and handle a bunch of security things such as CORS, CSP, and a whole bunch of other odds and ends!

As you might expect, unless you;re doing this stuff daily, its hard to keep it all in your head, so removing any piece makes life much easier.

One area I have found particularly useful is being able to use a framework such as React, but without needing a build step. Doing this is easier than it might appear, and only takes one or two little tricks to make it work.

Removing JSX

The first piece of the puzzle is removing JSX. JSX looks like HTML, but actually gets compiled to a bunch of calls to Reacts createElement function. In turn this builds out the Virtual-DOM, and React then does the diff/update steps etc. Without a build step we need some way to do that in the browser.

Htm is a small library by Jason Miller that takes tagged template literals that look pretty close to JSX, and turns it in to the createElement structure used by React!

There is one minor different in that you need to refer to a component as <${myComponent}> instead of plain <MyComponent>, but otherwise everything behaves the same.

Htm needs to be bound to createElement to make it work so its good to do that in its own little module and export the html function in a file called html.js:

import htm from 'https://esm.sh/htm@3.1.1'
import { createElement } from 'react'
export const html = htm.bind(createElement)

Import Maps

Normally without a build you would have to import React like this:

import { useState } from 'https://esm.sh/react@18.2.0'

This is a bit clunky and cumbersome, so instead we can leverage import maps to make that look a bit nicer.

In the head section of your html document include the following:

<script type="importmap">
  {
    "imports": {
      "react":"https://esm.sh/react@18.2.0"
    }
  }
</script>

In your JS files you can now do:

import { useState } from 'react'

A Simple App Example

To test all this out we need a simple hello-world example. Create a file called App.js containing the following:

// @ts-ignore
import { html } from 'html'
import { useState } from 'react'

const Counter = () => {
  const [count, setCount] = useState(0)

  return html`
    <button onClick=${() => setCount(count + 1)}>
      Click Me [${count}]
    </button>
  `
}

export const App = () => html`
  <div>
  <h1>Hello (Build-less) World!</h1>
    <${Counter} />
  </div>
`

This contains two components, the App component which we will call to start our app, and a local Counter component. You can see on line 18 that we needed to reference the Counter component as a variable rather than an element name, but otherwise it looks exactly like a normal React component.

Tying it all together

As normal, we bring this all together with an index.html document which contains the import map, the root div that will contain the application, and a small inline bootstrap script to kick it all off:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script type="importmap">
    {
      "imports": {
        "react": "https://esm.sh/react@18.2.0",
        "html": "/html.js"
      }
    }
  </script>
  <title>Build-less React MVP</title>
</head>
<body>
  <div id="root">This will be replaced</div>
  <script type="module">
    import {html} from 'html'
    import {createRoot} from 'https://esm.sh/react-dom@18.2.0'
    import {App} from './App.js'
    const root = createRoot(document.getElementById('root'))
    root.render(html`<${App} />`)
  </script>
</body>
</html>

Note: we import react-dom with a full URL since this is the only place we’ll use it, so it isnt necessary to add it to the import map.

As we go though app development, we can add other external scripts to the import map. for example we can import react-router-dom by adding:

{
  "imports": {
    ...
    "react-reouter-dom": "https://esm.sh/react-router-dom@6.4.1",
    ...
  }
}

and then :something like

import { html } from 'html'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { MyComponent } from '/Components/MyComponent'
import { NotFound } from '/Components/NotFound'

export const App = {} html`
  <${BrowserRouter}>
    <${Routes}>
      <${Route} path="/" element=${html`<${MyComponent} />`} />
      <${Route} path="*" element=${html`<${NotFound} />`} />
    <//>
  <//>
`

Running Locally

As a final touch, all this can be run locally using something like the http-server utility from npm. You’ll need to run it in SSL mode which requires an SSL certificate which can be created with:

openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem

and then start the server with the command:

http-server -S -C cert.pem

779 Words

2023-12-12 09:48 +0000