Caleb

My fair share of rehydration in React

October 22, 202210 min read

I've always been fascinated by the kind of work that people like Kent C. Dodds and Josh W. Comeau put out there in the field of Developer Education. This article encompasses how my curiosity almost landed me in a very deep and recursive process of debugging to no avail for almost two days, but, that's sort of what we do right? — getting stuck over and over until we find a possible fix to a problem. LOL!

Okay, enough with the story, "My fair share of rehydration in React" you'd have probably muttered to yourself. Well, all these started when I took a look at Kent's Blog, and I loved how simple the UI of the featured-article card appeared, but that was not the only thing. I figured out that when I refreshed the page, the featured article changed, immediately, I made the "wow this looks sooo cool" face.

I thought to myself, I can recreate this on my blog too, little did I know that it will appear a little bit more complex than how I first perceived it to be, then. Nonetheless, I embarked on this arduous journey.

Thinking in React

I read the first module — Thinking in React — on the beta version of React's documentation website by Dan Abramov. The whole idea behind — or at least, what I perceive it to be — this, is for us to be able to understand and develop a mental model for problem-solving, that way, we can always attempt to proffer solutions to similar problems without having so many blockers, rather than just jumping into the process of writing the code.

Establishing the mental model, or pattern of implementing this feature took almost three pages in my notebook, as simple as the solution is, yes. I see my mental model as a decision tree, though. The first thing I knew that I had to do was to create an entirely new UI that would take in the featured-article data which I'm pulling with Next.js' server-side data-fetching method, getStaticProps().

Creating the layout of that component wasn't the issue, and neither was the process of returning the latest article by selecting the first index in the array of blog posts. I recalled that the moment I refreshed Kent's blog, the featured article is a new one, completely different from what was there during the initial page load.

I became awestruck, again! How do I implement this exactly? I figured out that the best solution I can use for the implementation of this feature was to rely on JavaScript's Math.random() method. I'll pull data from the posts prop via getStaticProps(), assign it to a variable, and multiply it by the length of the array.

export default function BlogPage({ posts }) {
  const randomFeatured = Math.floor(Math.random() * posts.length)
  let featuredPost = posts[randomFeatured]

  return (
    <React.Fragment>
      <FeaturedCard data={featuredPost}>
    </React.Fragment>
  )
}

The randomFeatured variable returns a number between 0 and the length of the array. Say, for example, I have a total of ten blog posts, and I'll be getting a number between 0 and 9. Next thing was to access that random number with the posts array, like so;

let featuredPost = posts[randomFeatured]

"Whew! that was easy" was the phrase I muttered to myself until I got stuck. But, let's not go there yet. I decided to make my whole idea behind the implementation of this feature to be around this: I want people to come to my blog and read the articles there without having to keep scrolling until they find what they want. Fine, the idea of a featured article gets the attention of people, but I also want that <FeaturedPost /> component to be dynamic.

That's the whole essence of the randomly generated featured posts. With that step out of the way, I thought of another feature to add to the overall process of rendering the rest of the articles, I wanted a way to render the remaining articles without the featured article in this list.

Wow! It felt really simple — at least, that was what I thought. I was wrong, again! My original approach towards accomplishing this goal spiraled — aimlessly — around the usage of so many unnecessary array methods like .indexOf() — to get the current index of the featured article, Array.prototype.splice() — to mutate the content of the original posts array by checking if the properties of the featuredPost object were present in it. I ended up having something similar to the snippet below.

let featuredPost = posts[randomFeatured]

const indexOfFeaturedPost = posts.indexOf(featuredPost)

if (indexOfFeaturedPost > -1) {
  return posts.splice(indexOfFeaturedPost, 1)
}

Ideally the .splice() method returns 1 when the condition in the if() block, above, evaluates to true, and -1 when it is falsy. But, this approach was wonderfully flawed, YES, it was. Why? you might ask me. Although this seemed to work at first, whenever the components are re-mounted in DOM, the .splice() method repeats the same process and keeps removing any featured article from the list. I was furious and frustrated, and I think I screamed too!!!

After several attempts toward losing it all. I tried using the Array.prototype.filter() method, — one I should have utilized from the get-go. This method provided everything I needed for the success of this feature.

const updatedPosts = posts.filter((object) => object.id !== featuredPost.id)

Initially, the frontmatter of the articles on my blog do not have the id property in them, so, for me to be able to implement this without any blockers, I had to make sure that these ids were consistent throughout all the articles frontmatter. In the snippet above, you'll notice that I'm comparing the id of the featured article with the rest of the array content in the posts variable, and proceeding to return a new array — updatedPosts

With that sorted out, I moved on to use React's useEffect() hook and the setTimeout() function to update the featured article, after thirty seconds. Although the featured article gets updated when you navigate to another route on my website, and back to the /blog route, since the variable is in an effect lifecycle method, which gets called whenever a component is mounted on the DOM.

React.useEffect(() => {
  setTimeout(() => {
    featuredPost
  }, 30000),
    updatedPosts
}, [])

I went ahead to add the updatedPosts variable in the lifecycle method too. Everything seemed fine, I was super pumped that I had been able to accomplish something meaningful — If only I had known that the next set of outcomes would be disastrous.

My travails with rehydration

As I said previously, little did I know that the constant update of the featured article would result in a rehydration error, take a look at it in the image below. I had zero knowledge about the consequences that my approach would yield, and this error spanned out of the Server-side Rendering approach that I'm using to get my articles with Next.js' server-side data-fetching methods: getStaticProps and getStaticPaths.

rehydartion error in Next.js

I needed to understand why this kept happening because I can remember clearly that wasn't the first time it's happened to me. I knew beforehand that Next.js uses a SSR: Server-side Rendering technique to optimize the experiences we ship to people around the world, when they use the software we've created, on the web.

Before the advent of Frameworks like Next.js and Gatsby, the rendering process of React applications occurs on the client-side — in the browser. Meaning that, when you load or visit a React application, the browser gets an initial HTML document which is due to the inner workings of something similar to this snippet below, in a React app, bootstrapped with CRA: create-react-app

import ReactDOM from 'reacct-dom'
import App from './App'

ReactDOM.render(<App />, document.querySelector('#root'))

On initial load, the page is empty until some of the scripts that provide functionality have been fetched and parsed by the browser. While this — the parsing process of scripts — is going on, React is creating a representation of how the page would look like, when the render-blocking scripts have been parsed successfully.

But, this process — between the browser and React — takes time, which in turn leads to a poor user experience for people, because while this is going on, the user gets to see a blank screen alone.

With server-side rendering(SSR), the user, or whoever visits your site or app, gets a full HTML document from the server, instead of the empty one with a bunch of scripts that needs to be parsed by the browser. With the server-side rendering approach, the user is presented with an HTML — sometimes with the styles too — document that they can look at, while the browser and React does their thing.

Back to the problem on ground now, shall we? The error message in the image above says "The text content does not match the server-rendered HTML", and this is a result of the random featured article cards I'm rendering, which in turn causes a mismatch between the server-rendered HTML and this new version of the page, every thirty seconds and every time the user navigates away, and back to the /blog route.

A proposed solution

So, how do I fix this? was the question I asked myself. Knowing that the problem was related to hydration was the first step towards my liberation, I started making my research, and I found this article written by Jason Lengstorf.

It had the exact error message as the title of the article, I felt so happy at that moment, not until I read through it and figured out that, his approach did not suit my use-case. His approach involved using Next.js Advanced Middleware on Netlify and writing Netlify edge functions to transform a page on the fly, at the edge.

But, I stumbled upon an article — The perils of rehydration by Josh W. Comeau where he buttressed on how the idea of rehydration does not equate to be another render, the idea behind Server-side Rendering and some other things I can't remember. You can take a look at the article here.

The solution looks similar to the snippet below, where a state variable is initialized — in the component that will be rendered — and is set to be false by changing the value of the variable in the useEffect() hook to be true, we trigger a re-render with the content of the component.

const ComponentToBeRendered = () => {
  const [isComponentMounted, setIsComponentMounted] = React.useState(false)

  React.useEffect(() => {
    setIsComponentMounted(true)
  }, [])

  if (!isComponentMounted) {
    return null
  }

  return (
    <ContentOfComponent>
  )
}

Improving user experience

In the solution that Josh proposed, I figured out that it caused a layout shift on my blog whenever the featured article is yet to be mounted. The GIF below illustrates that and this is due to the null value that was returned in the if() block.

layout shift

Instead of returning null, I decided to create a skeleton loader that will give the user or the readers an idea of what to expect concerning the layout of my blog.

if (!isComponentMounted) {
  return <LoadingFeatured />
}

With this solution, I was able to bypass the rehydration error in React and provided a good UX by returning a loading skeleton.