React: Re-render a Component On Route (or props) Change

Published Feb 22, 2018Last updated May 26, 2018
React: Re-render a Component On Route (or props) Change

A common scenario with React Router: re-render a component on route change.

The same concept applies for re-rendering a component when props change.

In other words: reloading/refreshing the same React component when visiting a link.

How do you write such logic? Let’s find out together!

Originally published on valentinog.com/blog on February 20, 2018

React: re-render a component on route change. The use case

React promotes component reusing.

You don't want to write 2 different components for calling 2 separate endpoints.

It makes sense to have a single React component for calling the API endpoints depending on the location pathname.

Consider an application in which React router handles the routing part.

When visiting https://example.app/subscribers I want to call api/subscribers/.

And by clicking https://example.app/leads I want to call api/leads/.

Here's the code:

import React, { Component } from "react";
import ReactDOM from "react-dom";
import { HashRouter, Switch, Route } from "react-router-dom";
import TableContainer from "./TableContainer";

const App = () => {
  return (
    <React.Fragment>
          <Switch>
            <Route exact path="/subscribers" component={TableContainer} />
            <Route exact path="/leads" component={TableContainer} />
          </Switch>
    </React.Fragment>
  );
};

export default App;

ReactDOM.render(
      <HashRouter>
        <App />
      </HashRouter>,
      document.getElementById("app")
    )

As you can see TableContainer is the only React component for fetching data.

TableContainer acts as a container for another component: Table.

More about presentational and container components: writing React components

It should call the appropriate endpoint whenever I click /subscribers or /leads.

How? Sounds easy. React Router makes this.props.location.pathname available to its children.

Plus we know that componentDidMount is a good place for making AJAX calls in React.

That leads me to write something like:

import React, { Component } from "react";
import Table from "./Table";
import axios from "axios";

class TableContainer extends Component {
  constructor() {
    super();

    this.state = {
      data: [],
      loaded: false,
      placeholder: "Loading..."
    };
  }

  componentDidMount() {
    this.getData(this.props.location.pathname);
  }

  getData(pathname) {
    axios
      .get(`api${pathname}/`)
      .then(response => {
        this.setState({ data: response.data, loaded: true });
      })
      .catch(() =>
        this.setState({
          placeholder: "Something went wrong, please try again later"
        })
      );
  }

  render() {
    const { data, loaded, placeholder } = this.state;
    return loaded ? <Table data={data} /> : <p>{placeholder}</p>;
  }
}

export default TableContainer;

With the application in place I can see the expected data if I click either on /leads or on /subscribers for the first time.

But what happens if I click one of the links again? Nothing.

The table is still there and does not update itself. Why?

Because the component is already mounted and React won't render it again, even if I keep clicking.

We need to find a workaround. What if we can re-render the component on route change?

React: re-render a component on route change. componentDidUpdate to the rescue?

Let's head over the documentation for React lifecycle methods.

What can we see?

Again, componentDidMount is a good place for making AJAX calls but the problem is, clicking on a link does not trigger a remount.

Then there is componentDidUpdate.

componentDidUpdate seems interesting. According to the documentation "it is invoked immediately after updating occurs".

Looks like componentDidUpdate would trigger a re-render since React router updates this.props for every children.

And of course it will.

Armed with that knowledge you may be tempted to put another AJAX call inside componentDidUpdate:

  componentDidUpdate() {
    this.getData(this.props.location.pathname);
  }

What do you think it will happen? No need to try by yourself, I already did.

Making AJAX requests and consequently calling this.setState inside componentDidUpdate will cause an infinite loop.

I bet it will. componentDidUpdate is invoked immediately after updating. The component will keep refreshing and calling the API forever.

There's one possibile solution for avoiding the loop. You can compare prevProps with current props. But in all honesty I don't like the approach. I want my code to be as clear as possibile.

React: re-render a component on route change. A cleaner approach with componentWillReceiveProps

Let's check again the React lifecycle methods.

There's another method that seems interesting too: it's componentWillReceiveProps.

componentWillReceiveProps is another React lifecycle method. According to the documentation it is invoked before a mounted component receives new props.

Seems like a good candidate for calling an API when this.props.location.pathname changes. In fact the signature for the method is:

componentWillReceiveProps(nextProps)

That means it is possible to read the new pathname with nextProps.location.pathname.

The workflow becomes clear:

  • call the API inside componentDidMount when the component mounts the first time
  • call the API again inside componentWillReceiveProps when the component is going to receive new props

Now the component will look like the following:

import React, { Component } from "react";
import Table from "./Table";
import axios from "axios";

class TableContainer extends Component {
  constructor() {
    super();

    this.state = {
      data: [],
      loaded: false,
      placeholder: "Loading..."
    };
  }

  componentDidMount() {
    this.getData(this.props.location.pathname);
  }

  componentWillReceiveProps(nextProps) {
    this.getData(nextProps.location.pathname);
  }

  getData(pathname) {
    axios
      .get(`api${pathname}/`)
      .then(response => {
        this.setState({ data: response.data, loaded: true });
      })
      .catch(() =>
        this.setState({
          placeholder: "Something went wrong, please try again later"
        })
      );
  }

  render() {
    const { data, loaded, placeholder } = this.state;
    return loaded ? <Table data={data} /> : <p>{placeholder}</p>;
  }
}

export default TableContainer;

Now the TableContainer component:

  • calls the corresponding endpoint on first mount
  • calls api/subscribers/ if Route changes to /subscribers
  • calls api/leads/ if Route changes to /leads

Neat!

React: re-render a component on route change. Wrapping up

React promotes component reusing.

You won't code 2 different components for calling 2 separate endpoints.

A single React component can fetch the data from different endpoints depending on the location pathname.

AJAX calls in React are usually done in the componentDidMount method.

The problem is: visiting a new route does not make the component to mount again. So what?

Here's a solution: forcing a component to re-render on route change.

To recap, how do you write such logic?

Solution 1: componentDidUpdate

  • call the API inside componentDidMount when the component mounts the first time
  • call the API again inside componentDidUpdate when the component receives new props

But... making AJAX requests and calling this.setState inside componentDidUpdate will cause a loop.

To avoid the loop you should compare prevProps with the current props. And we know that comparing two values/objects or whatever is always hard and somewhat messy.

We can do it better.

Solution 2: componentWillReceiveProps

A neat approach:

  • call the API inside componentDidMount when the component mounts the first time
  • call the API again inside componentWillReceiveProps when the component is going to receive new props

You can simply access nextProps.location.pathname rather than comparing prevProps with the current props.

That leads to a more cleaner code.

Don't you think?

Plus, you can apply the same logic for re-rendering a component when props change.

Use your tools wisely.

Thanks for reading!

Originally published on valentinog.com/blog on February 20, 2018

Discover and read more posts from Valentino Gagliardi
get started