Using React, Redux and SSR to acommodate users without JavaScript

Whether or not something works without JavaScript (JS) is something that pops up in Hacker News comments from time to time - mostly when they don’t.

Who are these people with JS disabled, and why aren’t things working without it? The former are surely a minority, but even so, with NoScript being the 7th most downloaded Firefox Add-on and other browsers and ways of disabling JS existing, these users aren’t negligible. For the latter - I don’t know. Maybe modern web developers are lazy. Maybe they’re overworked, and pressed on time with too low budgets. Maybe supporting noscript is too hard. Maybe they don’t even know JS can be disabled or why anyone would do it.

I don’t claim to have the answer to these questions, but for some time I’ve had an idea on how to accommodate these users in a way that isn’t too much of a burden on the developer. The following is a short exploration of a method to do that, along with a demo showing it off. It’s something I hope to implement for my Wishy.gift, my side-project, at some point in the future, but more importantly, I hope someone can come up with something better.

Just want to see the demo? Click here, or check out the source code.

JavaScript

Saying that JS is used heavily is putting it mildly, with 95.8% of all websites using it client-side. I’m sure many of these websites work without JS, which is great. On the flip side, there are definitively web apps that can’t work without JavaScript, like WebGL/canvas experiences, audio editors, gamepad testing, and other web apps that rely heavily on JS APIs in the browser. Somewhere in between these 2, you have CRUD apps (of varying complexity) that don’t, or only partially work without JS.

A basic component of every single one of these web apps, is to render HTML, which usually reflects some state. Frameworks like React are often used in modern web development to help with the rendering and interactivity. Many of these frameworks have ways of handling the state, and many also allows the state to be stored elsewhere, like in Redux - “A Predictable State Container for JS Apps” that employs the event sourcing pattern.

An event-based state container like Redux is very well suited for our purposes because it aligns perfectly with a kind of interaction that works in the browser both with and without JS - Form submissions! Everything put into the redux store should be serializable, and anything we submit in a form is also so - at least with enctype="application/x-www-form-urlencoded". Although there is no “right” answer to whether or not you should put all your state into Redux, if we do - and it might certainly be more work - we get a centralised store that can run both client- and server-side.

This store can then act as the baseline or single source of truth for the state that our rendered HTML reflects. Given that React (and other) frameworks support server-side rendering, we can - with some cleverness, foresight, and care - create a single React app that works both with and without JS (with a whole lotta loadin’).

How

First we create helper components that will abstract away the main difference between having and not having JS. For implementing the classic TodoMVC app, I only needed the Button and Form components, the first one being a wrapper around “do this on click”, and the latter being a more general wrapper for actions relying on more input from the user. Both of these helpers work on the same principle of rendering a form containing the entirety of the action - type and payload - as form elements. When the React app is running client side, the submit events will be prevented, and the actions dispatched client side. Without JS, nothing prevents the event, and a request is made towards the server on whatever route we’re currently at.

Server side, in addition to listening on GET requests on view routes, we also listen to POST requests. For all GET requests we return the initial React app that will be rehydrated for any user with JS. We do the same for any POST request, but we also make the assumption that the only reason anyone would make a POST request towards any of these routes, is because they’ve submitted a form. With that assumption, we check if there was indeed a body in the request containing a type, and potentially a payload and payloadType too. If we find this, we check if we have some state on the current session, and use that to initialize a Redux store. Then we dispatch the submitted action to the store, fetch the resulting state, update the session state, and use the store to render our app.

You can check the demo that implements all this here, and check out the source code here.

Caveats

  1. We don’t have access to any JS-events - only forms being submitted, which means that no essential interaction should require things like dragging, double clicking, long clicking, etc.
  2. We get some extra markup that we have to take into consideration when styling, as everything must be wrapped in a form - we can’t have standalone buttons with onClick handlers.
  3. The server-side session management can probably be done in a more performant way than how it is in the demo, and in production you might want some more sanitization and validation of the submitted actions.
  4. There will be an endless stream of refreshes for users without JS if a web app is implemented using this method, but in many cases the alternative is no web app at all.
  5. Implementing this will most definitively increase the server load, and thereby cost.
  6. This can surely be implemented using other frameworks than React and Redux. Perhaps this might even be something that could find its way into Sapper or Next.js ?

Summary

In short, by leveraging the similarities of form events and an event based state container like Redux, we can dispatch and update state on the server side for users without JS enabled, by making all actions go through forms. This is made easier by making use of some helper components to catch, prevent and dispatch the events client side, and makes it so that there’s little - if any - double work that has to be done to reap the benefits. We are limited in terms of what kind of interactions we can make use of for the most essential functions, and have to take some extra precautions with regards to both styling, security and server cost, but that might be preferred over the app not working at all for users without JS.

Thanks and further discussion

Thank you for reading this! I would love to hear your thoughts and ideas too. Join the discussion on Hacker News, or feel free to email me at <firstname>@klungo.no, or DM me on Twitter