Mutahhir Hayat Central

Spatis: What's rendering got to do with it?

January 30, 2020

Since the last post, I’ve been trying to get to a version of Spatis that is able to serve some react, and is able to handle itself as a single page application. This part will deal with the react part.

A bit of an update first: There have been a few interesting gotchas, some easy to solve, others that made me fear this might actually might not be viable. The good news, however, is that I’ve been able to get a very simple example code to run. Yay!

First things first, I needed a server. The goal was to get something basic that was able to run the example, but still designed enough that I can extend it as I find more things to add to it.

The first problem to solve was routing. The API I’m currently using to register a route is:

const app = new Server(); 

app.when('/foo', () => {
  return (
    <div>
      <p>Foo bar!</p>
      <a href="/">Go to main page</a>
    </div>
  );
});

I might change to a different verb for specifying the route, but instead of using get, post and such REST verbs, I decided to use when because we’re only routing to actual pages, and Spatis would create an abstraction for API routes. So, ideally, using app.when(<PATH>, () => <REACT>) is all anyone has to do to get their app up and running.

Routing turned out to be simpler to implement than expected. I used the popular path-to-regexp NPM package and it gave me all the methods I needed to be able to match incoming urls to their actual handlers.

One of the few things I had to figure out were how to separate out the API and Static asset endpoints. API endpoints are going to be called by our single page application (SPA) to provide data, and static assets would be called to get things like Javascript files, CSS files, etc by the SPA. For now, I made some standard choices, like /api for API routes, and /static for static assets. Seems pretty standard, but I’ll see if that changes as I make progress.

Once I got some routing to work, the next thing was to actually serve content to the browser.

I want to keep Spatis simple to work with, that’s one of the over-arching goals. Things like Babel configurations, webpack configurations etc while super duper useful, don’t make it simple to start projects. I naively thought I could skirt by those requirements, until I tried doing node example.jsx and it threw up syntax errors on my CLI. Of course I can’t use JSX natively in JS. Ugh.

via GIPHY

This has been one of the biggest concessions I’m making right now (probably not the last though). The inclusion of Babel. I’m trying not to think about how customers would have to do this, but for now I’m just using the @babel/register hook and skipping the ‘build’ steps that come with webpack and babel. Unfortunately, I don’t see a way around this because the alternative is to write React code without JSX. That’s a worse developer experience. In the end, I might be able to get Babel transpiration to be setup automatically using CLI helpers etc. Similar to how create-react-app builds the initial package. It is not ideal, but such is engineering.

Once babel was in place, I was able to get static HTML pages rendered by React components to work. React does most of the heavy lifting by rendering components to strings. Here’s how it works:

  • Write component
  • ReactDOMServer.renderToString()
  • Wrap component in some HTML (<html><head>...<body>...</html> etc.)
  • Send the html string to the browser, it knows how to render it.
  • Load the React JS code for the component, and call ReactDOM.hydrate(element, container)

Now, the last bullet point made me realize again that I hadn’t thought through the problem. First, why do we need to call hydrate?

via GIPHY

Hydrate is pretty cool as it converts a string that renderToString() created in the server, and attaches all the events to them on the browser. This way, your components work as if they were created by React code running on your browsers.

What was the problem though, this seems simple enough? Yes, but… I was only sending a rendered string to the browser, and no JS code that worked on the browser. When you usually write React code, you write it in a Javascript file that get’s executed on the browser. It does things like createElement and attaches events to those components. Events that tell react when to re-render the component and do all its virtual DOM magic.

Now, it’s one thing to just execute some Javascript code on your server (where you wrote it) and send back the result to the browser, but it’s a different thing altogether to send the code you wrote. Fortunately, Function.toString() returns the full function and all the code as a string. I love Javascript! Here’s a simple flow:

  • Call toString() on the handler.
  • Send to browser
  • Write browser code to put string within a <script> tag

As usual, the last bullet point is always the stickiest. It exposes the question: “Who is going to request the code, and who will add the returned code to a script tag?“. The short answer: BrowserRouter. However, I’ll get into the BrowserRouter in the next post. For now, let’s just say we have some code running on the browser that can request the javascript code, create a script tag, and place the javascript in the tag.

The really cool part is that if you put some code within a script tag — even one that you’ve generated using Javascript — the browser automagically parses and executes it. Did I mention I love working in Javascript?

With the component code coming over and React hydration working, it was a major milestone, but it also opened up a slew of problems. Some I had already solved (insert air quotes) by the time I got here, and others that I’m still struggling with.

Next post will deal with a problem I’ve already gotten a handle on (more air quotes) the whole two routers issue. One router that we code on the server, and the second router that exists on the browser, the mysteriously named BrowserRouter.


Mutahhir Hayat

Written by Mutahhir Hayat.
Wakes up early, doesn't always sleep too well
Twitter?