I needed to build a new react site so I fired up creat-react-app in my terminal. While I was waiting for all those node modules to install, I started reminiscing about the old days where you didn't need fancy jsx and 1000 line bundle.js file just to build a Hello World site. Now don't get me wrong I love the ease of use of npm and all the luxuries it provides. The main thing that annoys me is waiting for the project to rebuild after every change. Now I have heard about snowpack and how it improves on other bundlers, but I started to wonder if it is possible to write a full stack NodeJS and React application without a build step. This is what I came up with.
DISCLAIMER - Please do not use this in production. This is more of a proof of concept.
ES Modules in Node
ES modules have been fully enabled in node since version 12 as long as the file ends in .mjs
instead of .js
(Note: The feature is still considered experimental). This allows us to use full ES6 syntax import and export syntax without needing any compilation!!!
Here's the code I came up with for a minimal server:
import { resolve, join } from 'path'
import fastify from 'fastify'
import serve from 'fastify-static'
import s from 'socket.io'
const app = fastify()
const client = join(resolve(), 'client')
app.register(serve, { root: client })
const io = s(app.server)
let socket = null
io.on('connection', (soc) => {
console.log('Connected to client')
socket = soc
})
app.listen(3000)
One thing to note is that in .mjs
files global variables like __dirname
and __filename
are not available. The functions from the path module can be used to produce their values.
ES Modules on the Client
Look at the current support, we can see that 93% of users can run es modules natively in their browser.
JSX but not really
Once you have discovered the wonders of React and JSX no one really wants to go back to writing plane old HTML, JS and CSS. So how can we use React in the browser without compiling anything?
Well the problem here isn't React, it's JSX. The browser does not understand it. So all we need to do is to write React without JSX, simple. Well if you have ever looked at React code without JSX you would know it is annoying to write and difficult to understand at a glance.
So what do we do???
We leverage the amazing work done by the creator of preact and use the package htm. It uses tag functions to give us near identical syntax to JSX with some minor caveats. This library and many others can be directly loaded using an import from a CDN. The CDN I chose in this case was SkyPack. It is maintained by the same people that make snowpack
Ok confession time. I did say that I was going to use React before but in the end I went with Preact because of two reasons. Firstly it had a higher package score on SpyPack compared to React's score. And secondly because both the framework and renderer were bundled in one package, I wouldn't have to load multiple packages over the network which in React's case would be the actual React library and React-DOM.
Here's what a component looks like:
import { html, useState, useEffect, useCallback, css, cx } from '../imports.js'
const center = css`
text-align: center;
font-size: 40px;
`
const red = css`
color: red;
`
const grid = css`
display: grid;
grid-template-columns: repeat(2, 1fr);
height: 40px;
& > button {
outline: none;
border: none;
background: orangered;
color: white;
border-radius: 5px;
font-size: 30px;
}
`
export default function App() {
const [seconds, setSeconds] = useState(0)
const [minutes, setMinutes] = useState(0)
const [start, setStart] = useState(false)
const reset = useCallback(() => {
setStart(false)
setSeconds(0)
setMinutes(0)
}, [])
useEffect(() => {
let interval = null
if (start) {
interval = setInterval(() => {
if (seconds < 60) {
setSeconds((prev) => prev + 1)
} else {
setMinutes((prev) => prev + 1)
setSeconds(0)
}
}, 1000)
}
return () => {
if (interval !== null) {
clearInterval(interval)
}
}
}, [seconds, start])
return html`<div>
<p class=${cx({ [center]: true, [red]: start })}>
Timer${' '}
${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}
</p>
<div class=${grid}>
<button onClick=${() => setStart((prev) => !prev)}>
${start ? 'Stop' : 'Start'}
</button>
<button onClick=${reset}>Reset</button>
</div>
</div>`
}
To centralise all the network imports, I created a file called imports.js
and then re-exported all the modules that I needed. This means that if I ever need to change a CDN link of a package I only have to change it in one place.
Developer Comforts
Everyone loves auto-reloading on changes during development. No one wants to start and stop their application whenever they make change. So how can we accomplish this. For the server this is easy we can just use a package. I ended up using Nodemand because it was the only one I found that supported es modules. The client side implementation was a bit more challenging.
So what I came up with was this:
Server
if (process.env.NODE_ENV !== 'production') {
import('chokidar').then((c) => {
const watcher = c.default.watch(client)
watcher.on('change', () => {
console.log('Reloading')
if (socket !== null) socket.emit('reload')
})
})
}
Client
<script>
// reload client on file change
const socket = io()
socket.on('reload', () => window.location.reload())
</script>
So during development the server watches the client folder and if any changes are detected a socket message is emitted. When the client received the message it would reload the page. I don't particularly like this implementation of client side reload, so if you have a better idea I would definitely like to hear them in the comments.
The project can be found on GitHub. Feel free to play around with it.
it is complicated in pov