Progressive enhancement in React
Table of Contents
Introduction
In the web front-end stack — HTML, CSS, JS, and ARIA — if you can solve a problem with a simpler solution lower in the stack, you should. It’s less fragile, more foolproof, and just works. - Derek Featherstone, April 23rd 2014
To make a very long story short, progressive enhancement is the right way to build websites, and everyone should endeavor to do it as much as they possibly can. Hopefully this post goes some way to convincing you of that.
What is progressive enhancement?
Progressive enhancement is the practice of ensuring that all required core functionality of a web page is delivered through basic HTML and CSS first. From there, the experience is 'enhanced' for users with more modern browsers, better connectivity or newer devices by layering on advanced capabilities, styles, and JavaScript.
Here's an example, let's say we have this review element:
<div id="review-form">
<label for="review">Your review:</label>
<input type="text" name="review" id="review">
<button>Submit</button>
</div>
Our intention is to enable the user to be able to submit their review (and its data) asynchronously, so everything is dealt with in the background. That's good UX, and simple to implement by writing some JavaScript:
const reviewForm = document.querySelector('#review-form');
const reviewFormButton = reviewForm.querySelector('button');
reviewFormButton.addEventListener('click', (event) => {
const reviewInput = reviewForm.querySelector('#review');
const reviewInputValue = reviewInput.value;
// send reviewInputValue to the server via async post request
});Great! That works!
But wait, what if our JS file fails to load?
Imagine our CDN provider has gone down and can no longer deliver our JS to the user's browser, or their connection drops out just as they request the JS, what happens then?
Well, because of our markup, the user is stuck. They can't submit a review because we haven't written our HTML with progressive enhancement in mind, so they're frustrated, and we lose valuable user feedback because we didn't do our job properly.
This is where progressive enhancement comes into play. Here's our new markup:
<form id="review-form" action="/review/submit" method="POST">
<label for="review">Your review:</label>
<input type="text" name="review" id="review">
<button type="submit">Submit</button>
</form>
See the difference?
In our original markup, we used a div to wrap the review element, and relied on our JS to capture the information submitted by the user and deliver it to our backend.
Here, even if our JS fails to be loaded, the user can still successfully submit a review (albeit with a page reload) natively through the functionality provided out of the box by using the semantic HTML form element.
It also enables us to clean up and simplify our JS, allowing us to deliver the smallest payload possible and speeding things up:
const reviewForm = document.querySelector('#review-form');
reviewForm.addEventListener('submit', (event) => {
// prevent page reload
event.preventDefault();
const formData = new FormData(event.target);
// send form data to the server via async post request
});Why should I care?
Look, I know HTML semantics and not building everything JavaScript-first can seem a bit boring at first, but there's some good reasons why you should care about progressive enhancement:
Your website will make more money
If more users are able to use the functionality provided by your website across more device types, network speeds and browsers, by definition you'll be casting a wider net, ensuring everyone has a good experience, and make more money from it.
You will minimise your payloads
By using progressive enhancement, you're only delivering the functionality that the user is actually able to receive. Additionally, you're moving functionality from higher in the stack (e.g. by using JavaScript) to lower down in the stack (e.g. by using HTML or CSS), meaning you should, in theory, write less code, by leveraging the built-in functionality provided by HTML as often as possible.
You will speed up your website
Self-explanatory, really. Smaller payloads === faster download speeds.
It's the right thing to do
Progressive enhancement and accessibility go hand-in-hand. Writing semantic HTML not only enables progressive enhancement, but also ensures your website is accessible to as many users as possible.
It makes you look like you know what you're doing
One day you'll be asked about it in an interview. And now (hopefully) you can give a confident answer.
Isn't React all JavaScript though?
Yes.
React works by delivering the user a JavaScript file first, which then builds out a HTML file from JS-only components on the client-side (browser).
Progressive enhancement works by delivering semantic HTML first, and then layering on enhancements depending on whether or not the user is able to receive them.
It would seem then that the two concepts are at odds with one another: One delivers JavaScript first and then builds HTML from it, and the other requires HTML to be delivered first and then JavaScript layered on top of it.
Enter Server-Side Rendering.
Server-Side Rendering
This is the solution to the problem created by JavaScript-based frontend frameworks.
Instead of delivering the user a huge JS file first and then building out a HTML document from it in the user's browser, server-side rendering (or SSR) enables the React code to be parsed into HTML on the server, delivered to the end user, and then "hydrated" in the browser to enable the full functionality of React.
WTF is Hydration?
Hydration is the process of taking your dry, static, lifeless HTML document that you've just delivered to your user, and "hydrating" it with React functionality (JavaScript). This primarily involves attaching event listeners to elements and initialising the application's state among other things.
This way, even if the JavaScript payload fails to be delivered to the user, they should (if your HTML is semantic) still be able to use the functionality of your website even if it might look a bit rubbish.
What are the trade-offs of SSR?
As we all know, there's no such thing as a free lunch.
The bad news is that SSR adds significant complexity to your hosting and server setup. Whereas before you were just delivering the required JS to the end user and letting their browser do all the heavy lifting of building your web pages for you, SSR means that every page you send must be fully built-out and computed from the React component tree before you send it to the client.
This means that your server can no longer just be a CDN delivering HTML, CSS and JavaScript. You now need a "proper" Node.js backend to be able to build out and serve up the pages to the users.
Additionally, it also means that the HTML delivered by the server must exactly match the HTML React expects to see in the browser when it's delivered.
Here's an example:
function MyReactComponent(){
if (window.innerWidth < 700) {
return <h1>You're on mobile!</h1>;
}
return <h1>You're on desktop!</h1>;
}This will throw a dreaded "Hydration failed" error, because the window object does not exist on the server, meaning it'll blow up because window is undefined.
Furthermore, even if it didn't crash on the server, a user visiting on a mobile device would receive the "desktop" HTML string from the server, but the browser would attempt to hydrate it as the "mobile" HTML string. Because the markup generated on the server doesn't match what the client expects on its initial render, React loses track of the DOM tree.
We can solve this by leveraging React's useEffect hook, which allows us to defer the client-side logic until after the component has rendered (and the page is hydrated), and then enhance the page with the correct value, e.g.:
import { useState, useEffect } from 'react';
function MyReactComponent() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
// This runs strictly on the client after hydration
if (window.innerWidth < 700) {
setIsMobile(true);
}
}, []);
if (isMobile) {
return <h1>You're on mobile!</h1>;
}
return <h1>You're on desktop!</h1>;
}The good news
The good news is that there are a lot of very capable developers that have worked very hard to solve this problem for you and open-sourced their work for you to use for free. How nice.
Packages like NextJS handle it out-of-the-box for you, and serve pages using SSR by default. These packages are designed to minimise the overhead involved with implementing Node/React/SSR etc., and are now very easy to spin-up, develop and host.
These packages abstract away all of the hard work involved with building and configuring a custom Node.js server and allow you to ship full-stack code that works even if you only know how to build things in React:
Summary
Hopefully by this point I've managed to show you that Progressive Enhancement is the correct way to build things on the internet, and that with a small amount of configuration you'll be able to convert your client-side only React site into a "proper", fully-fledged, SSR-rendered, progressively enhanced site that's usable by everyone who reaches it.
Please bear in mind that this is a heavily simplified introduction. There are a great many nuances and and best practices that should also be considered when building things in a progressively enhanced way. Going into all of them here would mean you'd have been bored long before reaching this summary. But like all things, it's a journey, and in this case, it'll likely make yours and your users' lives better.
Live long and prosper.