A Practical Use Case for React’s componentDidUpdate
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:
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 thefetchPostData
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
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.
And thus ends the story of how I learned how to use componentDidUpdate
on a real practical problem. This was an interesting discovery for me and I hope you've learnt something new too!