As a user, it can be very disorienting when the "wrong" UI is briefly shown to the user: a login link is shown to an authenticated user, or a 404 error flashes before the page loads correctly. This issue is common in Gatsby applications, because of how Gatsby pre-builds HTML files.
In this video, we show how issues like this can slip through, and how we can solve the problem by skipping user-specific state during the build. Instead, we'll leave that spot blank, and fill it in later on the client, when we know what should go there.
While this tutorial uses Gatsby, the same lesson can be applied to Next.js, or any server-rendered React application.
Joshua Comeau: [0:00] Here, I have a typical hello-world Gatsby application. The only thing I've done is I've added a little logged in user state in the corner. When I run this in development, everything works fine, but a funny thing happens once I deploy this.
[0:13] Here, I have the exact same app except it's been deployed and you'll notice there's a bit of a flicker happening here. If I open the DevTools and throttle my Internet to give us a better sense of what's happening, we see a log in link that shows briefly.
[0:26] The reason this happens is that Gatsby builds at compile time, which means that it takes your application and generates a static HTML file, and it's going to serve this HTML file for every single user.
[0:50] When I think about what I would like to see happen over here. Here, I have another deployed copy, except in this one, and I will go ahead and throttle as well, you'll see that I just render nothing.
[1:00] I leave a spot blank. That spot stays blank until the client is able to run and figure out whether or not this user is logged in again. By doing that, we avoid showing user the wrong state.
[1:11] If we hop on over to our code, you'll see that we have this AuthMenu component. This is responsible for rendering that little user profile in the corner. In this contrived example, I'm just using React state and you can imagine this accessing local storage to figure out if we have a user or not, and I figure out this might come from context or through a prop.
[1:29] Depending on whether I have a user or not, I'll either render the logged-in menu or I'll render a login link so the person can log in.
[1:36] The problem is that the very first time this code runs, it happens either on my development machine or in the cloud somewhere, and so it doesn't have access to local storage. It doesn't know who the user is because it's building the same HTML for every user. Invariably, we wind up with this being false, which means that everyone sees the login link.
[1:55] Really, what I want to do is defer this decision, so that I don't even look at this code until we're rendering on the client. A common mistake people make is they think, well, why don't I just check for the window. If the type of the window is equal to undefined, then you can do something else.
[2:10] You can return a spinner, or in our case, since we don't want to render anything, we'll just return null. This might work for you, but it violates a principle that React expects when it's rehydrating which is when it's melting on the client after being server-rendered, it expects you to keep the HTML the same.
[2:29] With this, we're going to wind up violating that expectation. This can lead to some funky rendering issues, so better to play it safe. Instead, we're going to keep track of whether or not we've mounted. I'm going to keep a new bit of state called hasMounted and we're going to default this to false. Since by default, we haven't mounted.
[2:47] Then, I'm going to use useEffect, and useEffect runs after every rendering including the first. For dependencies, I'm going to pass an empty array so that the code that I write in here, only happens after the very first mount. What I'm going to do is setHasMounted to be true.
[3:05] Now, I have this bit of state that starts as false, and then immediately after the first render, it becomes true and then it never changes again. What I can then do is say if we haven't mounted, if hasMounted is equal to false, then I can return null.
[3:20] One annoying thing about working with Gatsby in this way, is that your code doesn't do the same thing in development. You have to build for production to test whether or not you're handling server rendering correctly.
[3:32] I'm sick of waiting for a graceful shutdown. You have to do yarn build, instead of yarn start. Then, I'm going to use the serve package to just serve the public directory. This just spins up a mini HTTP server.
[3:43] I'm going to do the same thing where I throttle on my Internet. You'll see that we've now solved this problem, where we're just rendering nothing for that first render. Then once the client is ready, it can fill in the details.
[3:57] It turns out this is a problem you're going to run into quite often because whenever you have any data that changes from one user to the other, you're going to have to do the same dance of avoiding rendering that into the initial HTML. Otherwise, it will flicker and show the same thing to every user.
[4:13] I like to create a component for this, I'm going to call it ClientOnly. This only going to take one prop is going to take the children as a prop. In the happy path when we're rendering on the client, it's just going to return those children. It doesn't need a wrapper or anything.
[4:28] Then, of course, we need to do something to make sure that we're not rendering ahead of time. For that, we can just reuse the logic we already had. I'm going to grab all these bits and pull them in. Now, it's the same deal. I have my ClientOnly component that will initially return null.
[4:45] After the component has mounted, I flip this to true which means this condition is no longer met, so we just return whatever the children are.
[4:51] Then, our AuthMenu component can go back to be exactly as it was, where this component has no knowledge about any of this. To use it, I have to go find where the component is used. Actually, I should create this in its own file.
[5:07] Very quickly, just create a client-only.js, dump this in, make sure it's being exported, and make sure I have the right dependencies imported. Then when I use this component, here is where my AuthMenu is being rendered. My job now is to make sure that I'm wrapping this inside client-only. By wrapping this component inside client-only, I ensure that the very first time this renders, this thing never gets put into the DOM.
[5:40] I can then repeat the now familiar dance of building and serving. Just like before, I have a slot that doesn't render anything until we're ready for it on the client.