Turn Progressive Enhancement up to 11

Kent C. Dodds
Kent C. Dodds

In this video, we explore how to make an image uploader a progressive enhancement piece rather than a feature that only works with JavaScript enabled. We discuss the user experience for users without JavaScript, the clever solution of using a combo box, and the implementation of a class name on <html> to allow you to style things appropriately based on whether the user has js enabled. We also address the issue of hydration errors and provide a (safe) hack to ensure the correct HTML is served and hydrated. Most of us don't need to worry about supporting users who disable JavaScript, but if you do have that requirement, the solution here is pretty straightforward. Inspired by @jjenzz

Feel free to check out the Epic Stack example to dive into the code.

Transcript

00:00 We've got this really cool image uploader, so I can drag and drop an image, and we get a preview of that, and you can upload it. It looks awesome. The way that works is with this onChange. So when we change the file that's selected on this input, which is what happens when you drag and drop an image, or if we click on it and select an image from the file chooser here, then that also triggers the non-change. In either case, we create a file reader, we read that image, and then set the preview image to that, and then our preview image is fed into this image tag.

00:31 And so that's how the preview works. And it's awesome. And I think that it's a really great user experience. But some of us really have a very strong use case for users who don't have JavaScript enabled. Not a lot of us.

00:46 There may be like 1% or 0.1% of us who actually really care about this use case. But if you do, then you're really going to be interested in how can I make this a progressive enhancement piece rather than a thing that just works only if you have JavaScript enabled? So first, let's take a look at what happens if I don't have JavaScript enabled. OK, so I've got this. I click on this.

01:09 And I'm going to say, hey, I want to upload this image. And yeah, we don't get anything. And that's because it requires JavaScript to handle that event, right? If I hover over this, we're going to see, oh, 8.png. That's the file that I dragged and dropped, or uploaded, clicked, whatever.

01:25 So that's going to work technically, but it's not a good user experience at all. And so we need some way to have a baseline of a functional app and then enhance it if JavaScript is enabled. And so how do you do this? We need to have some mechanism to style this differently or make the experience different for users who don't have JavaScript enabled. And Jenna has come up with a really clever way to do this.

01:51 So she's got a combo box here, which when JavaScript is disabled, actually just shows an input and a submit button that triggers a full page refresh and then the server is going to send back the results based on that search. And then if you do have JavaScript enabled, then we don't show that little Submit button, and we give the user a really awesome experience. Now, the trouble is we don't want the user to initially load the page. And then when our components start hydrating and everything, they're like, oh, I am on the client now, I know that JavaScript is enabled, so let's swap. And so the user would get a content layout shift if we were to do it that way.

02:30 So it's not the way we wanna do this at all. Instead, we're gonna follow what Jenna has suggested here with a document body class list as a script tag in our root. So we're gonna go to our root and right down here in our document, right in our head, we're gonna say scripts. And because this is React, we're going to say dangerously sent inner HTML, and here's our HTML, is this string of HTML that's going to say document, and instead of adding it to the body, I'm going to add it to the class list right here. And here it's saying remove nojs.

03:06 We're going to actually add hasjs, just like she has suggested in here. And so now we can come over here and If we take a look at our elements, we can see hasJS is not added, and that's because we do not hasJS. But if I turn this off and refresh, boom, now we hasJS. So that's awesome. And now if we take a look at this, we go over to some CSS and we do some CSS magic to make things look a little differently.

03:35 Let's bring that CSS in and now we have JS and so those that CSS doesn't apply everything works as expected but if we don't have see JS then this is going to load And there's no flash of the different state or anything like that because this script evaluates before the rest of the HTML document shows up. And so we're able to say, hey, let's add this hasJS right up here. And then it continues to parse the rest of the HTML of our document. So, we know whether we have JS or not before the CSS is even loaded, before the components or the HTML for the components are even loaded. So there's not going to be any flash of any kind.

04:15 And so yeah, this will still work. I can even drag and drop on top of that file chooser, and I get exactly the behavior that I would want, even without JavaScript. So this is really pretty neat for that use case where you actually really do care about users who have JavaScript disabled. Not everybody, again, really cares all that much about that, but some people really do have this requirement. And so it's really useful to know how can I do this without a flash of loading or the incorrect content for the users who have JavaScript enabled?

04:45 But we're not quite done yet. So as cool as it is that we've been able to do all of this, we have a little bit of a problem here with adding this hasJS class to our HTML. And that problem is with Hydration. So let's take a look at our console. Let's enable our JavaScript again.

05:05 And we're going to get ourselves actually two problems. One is we have a nonce requirement because we've got a content security policy that requires the nonce. So that's easy enough to solve. There we go. Refresh that.

05:19 Boom. Now it's gone. No problem there. But then we've got a hydration error. So this hydration error is saying, hey, on the server, you rendered the light hful overflow x hidden hasjs.

05:32 And then on the client, it didn't have that. And so what's interesting about that though is here we're rendering this and it's never going to have hasjs the way that we have it written. If we run this on the client it's always going to miss that class name. The reason it's saying the server sent us that is because this hasjs gets added before the hydration happens. And so React is assuming, well, I mean, if it's showing up before I hydrate, I'm the first thing that's supposed to happen.

06:01 It must have come from the server. So the error mess is actually a little bit incorrect. If we would take a look at the view source on this, we're going to see this is what came from the server. It does not have hasjs. But because React sees that class name before hydration, it assumes it came from the server.

06:19 In any case, we're in trouble, because we need to make sure that the server does not send hasjs, because that's like the entire crux of how this works. But then secondly, that the client has hasjs. So we need to know like based on whether or not we have information from the client, we need to know whether we should have hasjs. So what if we do this? Hasjs equals type of document, it's not equal to undefined.

06:49 Okay, so that actually this is more like is server or is client so we maybe we could call that and then we'd say is client then has JS otherwise we don't know so we'll just leave it as an empty string. Now this is a bit of a, honestly a bit of a hack, but it's a pretty safe hack because it's going to ensure that the HTML that is served from the server is what we need it to be, but also it ensures that the HTML that's hydrated by the time React shows up is also what it needs to be. And so it's a pretty safe hack. I'm okay leaving things the way that we have it here. And so now we have gotten rid of our hydration warnings.

07:33 And the actual problems that come with hydration warnings is not just we're putting a band-aid on the problem. No, we don't have warnings. Hey, no. Like, those hydration warnings actually mean React is behaving differently. You need to get rid of the hydration warnings.

07:44 So we've solved that problem. And then we've also made it so that we can have our application work just as well with or without JS. Of course like we do have a better experience with the JavaScript and again most of the apps that we're building really don't really care too much about whether the JavaScript is there or not. But if you have the use case where you really need to make your thing work with or without JavaScript and you really want to add some nice things with the JavaScript, then this is how you do it. You add a hasjs in a script tag to your document element.

08:18 You make sure you handle any hydration issues. And then you can use CSS to style things differently based on the experience that is available to the users that you've got. And you can give the users the best possible user experience based on that. So I hope that is helpful to you. Have a wonderful day.

More Tips