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.
Top comments (11)
Perhaps I’m missing something, but I actually think your client side reload is quite a clever use of socket.io. What makes you not like it?
I also quite like how the Preact version of JSX you use looks just like HTML with js template literals. Are there any downsides to it?
Its not too big of a deal it's just that since there is no code pruning with a bundler that code will stay there. This is fine if you are making use of sockets in your project but if it is not needed it's just so extra code in production that does nothing.
So the htm parser is not tied to preact in any way. It can be easily used with any had library like react. Performance wise it should be just as performant as a normal jsx implementation that has not been uglified and minified.
Yeah maybe you’ll find a way to re-architect it so the dev code doesn’t get deployed into production. Using a socket.io ping to automatically reload the client app though is quite neat.
I like the general idea of not having compile steps, so I think it’s a worthwhile exploration imo. Thanks for sharing.
I thought old fashioned way was pure HTML, CSS, JS
Technically this applications only contains HTML and JS
Hence the use of the word pure for not using framework and others 😂
That's what I did for fine. React can be overkill sometimes
You can always go further back, more old fashioned yet is to pin a physical note to a message board, but we all know what OP meant ;)
it is complicated in pov
Some comments may only be visible to logged-in visitors. Sign in to view all comments.