Scenario

Let’s take a scenario where we’re building a blog app. The blog will be very minimal, with these features:

When a user visits the homepage, they should see a list of posts and should be able to click on any of the posts to be taken to the blog post’s page. When a user visits a blog post page, they should see the blog details i.e. title and content, and a list of recommended posts at the bottom of the page. Clicking on a recommended blog post’s link should take the user to that post’s page.

Now that we’ve figured out the requirements, let’s proceed to build the app.

Building the App

We will need 3 components for our app: the home page, the blog post component and the app’s root component, which decides which page to render depending on the route. For this example, we’ll fetch the posts from a local file rather than making network requests.

Let’s start off with the root component, which is where we’ll mount the app to a specific DOM node.

import React from "react" import ReactDOM from "react-dom" import { BrowserRouter as Router , Route , Switch } from "react-router-dom" import Home from "./Home" import PostDetails from "./PostDetails" import "./index.css" const App = ( ) => ( < Router > < Switch > < Route exact path = "/" component = { Home } / > < Route path = "/posts/:postId" component = { PostDetails } / > < / Switch > < / Router > ) ReactDOM . render ( < App / > , document . getElementById ( "root" ) )

We’re using the ​​react-router-dom package to render either the Home or PostDetails component depending on the route. A request with a URL such as /posts/2 will be routed to the PostDetails component and the postId variable will take the value of 2.

Let’s look at the Home component, which will act as our homepage:

import React from "react" import { Link } from "react-router-dom" import { posts } from "./posts" import "./Home.css" const Home = ( ) => { return ( < div className = " App " > < header className = " App-header " > < h1 className = " App-title " > Blog Posts </ h1 > </ header > < div > { posts . map ( post => ( < div key = { post . id } > < Link to = { ` /posts/ ${ post . id } ` } > < h4 > { post . title } </ h4 > </ Link > </ div > ) ) } </ div > </ div > ) } export default Home

In this file, we fetch posts locally from posts.js and use the Home component render a list of links to each of the posts. Each post has an id attribute which is used to construct the URL using the Link helper from react-router-dom​

This is how our sample ​post data looks:

export const posts = [ { id : 22 , title : "Welcome to React" , body : ` React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes. Declarative views make your code more predictable and easier to debug. ` , } , { id : 23 , title : "An Introduction to React Router" , body : ` There are three types of components in React Router: router components, route matching components, and navigation components. All of the components that you use in a web application should be imported from react-router-dom. ` , } , { id : 24 , title : "Welcome to Redux" , body : ` It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger. ` , } , ] export const recommendedPosts = [ { id : 30 , title : "Angular" , body : ` Learn one way to build applications with Angular and reuse your code and abilities to build apps for any deployment target. For web, mobile web, native mobile and native desktop. ` , } , { id : 31 , title : "Ember.js" , body : ` Ember.js is built for productivity. Designed with developer ergonomics in mind, its friendly APIs help you get your job done—fast. ` , } , { id : 32 , title : "Backbone.js" , body : ` Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface. ` , } , ] export const allPosts = posts . concat ( recommendedPosts )

The final piece of the puzzle is the ​ ​PostDetails.jsx component:

Link to PostDetails.jsx

In this component, we’ll store the post’s details in the component state, and all the values are initially empty. To fetch the post’s data, we’ll need to extract the postId parameter from the URL. ​​​​​​​​​​​​​​react-router-dom stores the route params in the component’s props and it enables us to access this parameter via this.props.match.params.postId . Once we have the postId , we then find the post with a matching ID in the posts array, and set it in the component state. This is handled in the fetchPostData function.

In the ​​render method, we show the post’s details and a Recommeded Posts section. When you click on one of the posts in this section, you should be taken to that post’s URL and shown that post’s details.

You can try out the app in it’s current state at CodeSandbox and also view the live code in the Live Editor

However, we have a bug lurking somewhere in our code. Try clicking on one of the recommended posts and see what happens.

Figuring out the Problem

If you haven’t tried out the live app, the issue we have is that when we’re in a blog post’s page, clicking on a recommended post doesn’t seem to do anything. If you’re keen though, you’ll notice that the URL does change but the page content doesn’t change 🤔

When you click on a recommended blog post link, React Router re-renders the PostDetails route component with a new postId parameter. This parameter is passed to the component as props . We can be sure the component does receive new props since the URL changes. But why does the component fail to display the updated data?

The root of the issue here is that we fetch data only on componentDidMount which is not called when a component re-renders.

What we need to do to solve the problem is to fetch new post data when we detect that the postId parameter has changed. This is where componentDidUpdate comes in.

Solution

Here’s a comment I came across in the react-router issue tracker that let me down the right path.

You probably want to just respond to prop changes with componentDidUpdate to refetch data when the same route is active but with different parameters Ryan Florence

Therefore, to fix the issue, we have to add componentDidUpdate , where we’ll check if the postId has been updated and then fetch the details of the new post. Once we have the new details and store them in the component state, the component re-renders and shows the updated content.

Here’s the small addition we need to make to fix the bug:

componentDidUpdate ( prevProps ) { const { match : { params : { postId } } } = this . props ; const prevPostId = prevProps . match . params . postId ; if ( prevPostId !== postId ) { this . fetchPostData ( postId ) ; }

The componentDidUpdate lifecycle method takes in the previous props as the first argument. We extract the postId from the previous props, and compare that with the postId from the current props. If there’s a change, we fetch new data using the current postId parameter. This helps us update the page content when the component re-renders with a new postId parameter, and this fixes our bug!

You can try out the new bug-free version on CodeSandbox and check out the code on the Live Editor. If you’re interested in the accompanying code samples, you can access them on GitHub.