Pages

Tuesday, May 08, 2018

Free your React SSR from Express. Well, if you want to.

Recently, I've been playing around with this pretty good howto for Reduxless React SSR, which needed a few tweaks to work.

Once things were running, and I could tell in Chrome's network tab that I was receiving good, pre-rendered [by the server] content, I had some difficulty as an SSR n00b figuring out if the client was wasting time re-rendering or not. I take that back -- render was being called on the client in all of my test app's components, though with SSR they seemingly shouldn't, and I wanted to see if...

  1. The DOM was also changing, and
  2. If I c/sh/ould stop that initial render, even if it was "free".*

* In the second point, what I mean by "free" is that if render is called but nothing's changed -- the SSR completely created your initial state -- there's no practical impact on or for the user if the client wastes a few cycles after initial page render. Those wasted cycles are free, so to speak, as there was nothing else to spend them on. Not great, but no practical problem.

I messed around with the React extension in Chrome for a while, and it looked like things were moving quickly/not manipulating DOM after load. Adding SSR to the test app I'd initially written with client-side-only rendering felt like a win. But this exercise also told me I wasn't familiar enough with React's decision-making process for when it'd decide to call render. Why was render wasting cycles? I'd SSR'd, dang it!

Luckily, I bumped into How does React decide to re-render a component? at lucybain.com that discusses exactly that:

A re-render can only be triggered if a component’s state has changed. The state can change from a props change, or from a direct setState change. The component gets the updated state and React decides if it should re-render the component. Unfortunately, by default React is incredibly simplistic and basically re-renders everything all the time.

Component changed? Re-render. Parent changed? Re-render. Section of props that doesn't actually impact the view changed? Re-render. [emph mine -mfn]

Hearing React wasn't exceptionally choosy about when to refire render made me feel better about all the render calls I saw, even after SSR. My components seem to be rendering on the client side, but it was only in the virtual DOM. That would seem to have to happen to compare the client-rolled DOM to the existing SSR'd DOM.

That means when it comes to the client-side render calls after successful SSR, I believe the answers to my above questions are...

  1. No, the client is not changing DOM.
    • Pro tip: If you use hydrate, React'll yell at you in the console if what's rendered from the server is different from what's expected.
  2. If you want to stop that render, you use (duh) shouldComponentUpdate.

NOTE: Check that pro tip out closely. If you mount React with hydrate (a v16 addition), it should yell at you if you don't have DOM that matches what React expects.

Second, um, don't change the name of your components' render calls to hydrate like my, um, my friend did. That's stupid. You hydrate only where you mount React:

//ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.hydrate(<App />, document.getElementById('root'));

The name of your render functions in your components will stay render.


Why does one use truly universal React?

Now even though I've learned to use the magic of renderToString, while I was searching up best SSR practices, I caught James Nelson's Universal React: You're doing it wrong, which referenced these tweets:

Dan Abramov @dan_abramov
Replying to @james_k_nelson

.@james_k_nelson Actually this isn’t even the case. AFAIK Facebook is not currently using React server rendering.

5:19 PM - 20 Mar 2016

And this one:

Christoph Nakazawa @cpojer
Replying to @dan_abramov

@dan_abramov @james_k_nelson http://instagram.com  uses server rendering for SEO. We haven't found sr useful for real users.

6:41 PM - 20 Mar 2016

For me, the quick take-home is this:

  1. Universal React wasn't worth the proverbial squeeze...
  2. ... in 2016.

I'd like to see updates on those. Does Instagram still use SSR? Does it think that's smart? Is SEO still enough of a use case for the overhead, especially now that at least Google's spider renders JavaScript? Is the SSR done with Universal React?

This also gives you a clue of where we might go for SSR -- not universal React.

Wait, what? Are you pondering what I'm pondering?

If they called them sad meals...

No no! I'm thinking we should look into doing SSR without React a little more closely.


SSR is just string manipulation

Look, SSR is the simplest thing you've ever read more than three articles about. At its core, it's just string manipulation. Let's look at a little node express code from the article I initially referenced:

// render the app as a string
const html = ReactDOMServer.renderToString(<App />);

// inject the rendered app into our html and send it
return res.send(
    htmlData.replace(
        '<div id="root"></div>',
        `<div id="root">${html}</div>`
    )
);

Follow that?

  1. Create a string to represent rendered content.
  2. Have an html page template for your react. (Nothing new here.)
  3. Replace the root element where you'd mount React and put your rendered html string in the middle.

It's that simple. You could use anything to create that html string, not just React running on Express. All that has to happen is that the content in html, once couched in the html page's template, mimics exactly what React would try to make when it renders OR, get this, you could write your React components in a way that they ignore the DOM from SSR and let the SSR'd DOM alone until state is changed on the client.

Make sense?

The most beautiful upshot of this "discovery" is that you can use any server you want for SSR.

Want to create your SSR content for a React SPA with C# and Razor?

Shia says, "Just do it!"

(It really isn't rocket science here -- the advantages of this approach are essentially tantamount to those when you used Apache in 1997. "Hey look! You want static text?! I can make you static text!" Or maybe more like using Classic ASP in 1997. "You want text generated from a database?!! You got text generated from a database!1!" Wow, man. Groovy.)

But I thought Universal was architecturally superior?

I really like the possibility of using the same language on the server as the client. There seem to be lots of obvious, inherent advantages. For one, you can write your business validation logic once, and reuse it on the client & server.

That's beautiful. For example, you almost get client-side validation "for free" when you write good server-side validation. And if I had a quarter for every time server-side validation on a project lagged behind the client (this is so wrong, folks. SOOOOO wrong), I'd have a few bucks.

But there is overhead in fully supporting universal React once you get past the first, obvious wins. And, truth be told, I've slowly moved off of doing any real validation client-side in all but the most network-starved use cases. I love JavaScript, but I'm practical enough to realize that thin clients are the best clients. Have your client ready to handle validation payloads from the server, but don't have any business logic client-side if you can help it.

Once you get past the proverbial Universal siren song and then also realize that SSR is really, at its core, simply what it says -- html rendered on the server-side, not necessarily html rendered with the same complex code as the client, now on the server -- you will finally have the most flexibility for composing the best, most performant React sites.


Random, somewhat related, somewhat timely reading: Redux - Not Dead Yet!