DEV Community

Quintus Cardozo
Quintus Cardozo

Posted on • Edited on

Writing new websites the old fashion way

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)
Enter fullscreen mode Exit fullscreen mode

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>`
}

Enter fullscreen mode Exit fullscreen mode

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')
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

Client

<script>
  // reload client on file change
  const socket = io()
  socket.on('reload', () => window.location.reload())
</script>
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo Quintisimo / no-build

A full stack NodeJS and React app with no build step. https://dev.to/quintisimo/writing-new-websites-the-old-fashion-way-3f9d

Top comments (11)

Collapse
 
mjgs profile image
Mark Smith

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?

Collapse
 
quintisimo profile image
Quintus Cardozo • Edited

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.

Collapse
 
mjgs profile image
Mark Smith • Edited

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.

Collapse
 
crimsonmed profile image
Médéric Burlet

I thought old fashioned way was pure HTML, CSS, JS

Collapse
 
quintisimo profile image
Quintus Cardozo

Technically this applications only contains HTML and JS

Collapse
 
crimsonmed profile image
Médéric Burlet

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

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

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 ;)

Collapse
 
mshajid profile image
mshajid

it is complicated in pov

Some comments may only be visible to logged-in visitors. Sign in to view all comments.