111

I'm working on a react application using react-router. I have a project page that has a url as follows:

myapplication.com/project/unique-project-id

When the project component loads, I trigger a data request for that project from the componentDidMount event. I'm now running into an issue where if I switch directly between two projects so only the id changes like this...

myapplication.com/project/982378632
myapplication.com/project/782387223
myapplication.com/project/198731289

componentDidMount is not triggered again so the data is not refreshed. Is there another lifecycle event I should be using to trigger my data request or a different strategy to tackle this issue?

Rich
  • 5,316
  • 9
  • 35
  • 58
Constellates
  • 1,865
  • 3
  • 16
  • 28

12 Answers12

112

If you do need a component remount when route changes, you can pass a unique key to your component's key attribute (the key is associated with your path/route). So every time the route changes, the key will also change which triggers React component to unmount/remount. I got the idea from this answer

Community
  • 1
  • 1
wei
  • 2,782
  • 3
  • 20
  • 31
  • 3
    Brilliant! Thank you for this simple and elegant answer – Z_z_Z Sep 03 '18 at 17:48
  • Just asking, won't this be affecting the performance of the app? – Alpit Anand Mar 27 '19 at 03:45
  • 1
    @AlpitAnand this is a broad question. Re-mounting is definitely slower than re-rendering as it triggers more lifecycle methods. Here I'm just answering how to make remount happen when route changes. – wei Mar 29 '19 at 15:27
  • But its not a huge impact ? Whats your advice can we use it safely without much effect ? – Alpit Anand Mar 30 '19 at 03:41
  • 1
    @AlpitAnand Your question is too broad and application specific. For some applications, yes, it would be a performance hit, in which case you should watch props changing yourself and update only the elements that should update. For most though, the above is a good solution – Chris Apr 27 '19 at 12:55
  • this does not work, produce render loop infinite with erros, maybe for very light componentes works, it's not my case – stackdave Dec 19 '19 at 22:25
99

Here is my answer, similar to some of the above but with code.

<Route path="/page/:pageid" render={(props) => (
  <Page key={props.match.params.pageid} {...props} />)
} />
Breakpoint25
  • 1,170
  • 7
  • 10
  • 7
    The key here plays the biggest role ,because image that you have Link to in a profile page ,upon click it just redirect to the same route ,but with different parameters ,BUT the component doesnt update ,because React can't find a difference ,because there must be a KEY to compare the old result – Georgi Peev Nov 11 '19 at 15:21
  • This is a graet answer - has saved me from componentDidUpdate hell ;) – Tony Sepia Mar 01 '21 at 14:47
  • The best answer, because update the comp only when changes :pageid param. – Cássio Lacerda Apr 13 '21 at 22:02
86

If the link is directing to the same route with just a different param, it's not remounting, but instead receiving new props. So, you could use the componentWillReceiveProps(newProps) function and look for newProps.params.projectId.

If you're trying to load data, I would recommend fetching the data on before the router handles the match using static methods on the component. Check out this example. React Router Mega Demo. That way, the component would load the data and automatically update when the route params change without needing to rely on componentWillReceiveProps.

Filip Veličković
  • 1,447
  • 3
  • 24
  • 32
BradByte
  • 10,278
  • 2
  • 33
  • 41
  • 2
    Thank you. I was able to get this working using componentWillReceiveProps. I still don't really understand the React Router Mega Demo data flow. I see the static fetchData defined on some components. How are these statics getting called from the router client side? – Constellates Aug 31 '15 at 20:40
  • 1
    Great question. Check out the client.js file and the server.js file. During the router's runcycle they call the fetchData.js file and pass the `state` to it. It's a little confusing at first but then becomes a little clearer. – BradByte Sep 01 '15 at 13:26
  • Coming to this some months later, and being very inexperienced, I am having difficulty updating the examples with the changes to react-router with release of 1.0 - is it still possible now that Router.run has been replaced with render({routes}}, el) - can we still do the fetching before the router handles the match? – theTechnaddict Jul 05 '16 at 09:02
  • Yes, you would do it inside the match. I made an example once here: https://github.com/bradbumbalough/react-0.14-iso-test. Check out the `src/server.jsx` file. – BradByte Jul 05 '16 at 13:23
  • 13
    FYI: "componentWillReceiveProps" considered legacy – Idan Dagan Aug 21 '18 at 15:32
  • 5
    You should use ComponentDidUpdate with React 16 since componentWillReceiveProps is deprecated – Pato Loco Nov 29 '18 at 12:41
  • 1
    This works but has the problem that you need to add componentWillReceiveProps() or componentDidUpdate() for handling re-renders to every component that is loading data. For example think about a page that has several components that are loading data based on same path parameter. – Juha Syrjälä Jan 06 '19 at 09:28
  • @PatoLoco that's exactly what I needed - I didn't need to remount or push a new route after all, I just had to check the `:parameter` value in `ComponentDidUpdate` – Paul Tomblin Dec 30 '19 at 20:22
15

You have to be clear, that a route change will not cause a page refresh, you have to handle it yourself.

import theThingsYouNeed from './whereYouFindThem'

export default class Project extends React.Component {

    componentWillMount() {

        this.state = {
            id: this.props.router.params.id
        }

        // fire action to update redux project store
        this.props.dispatch(fetchProject(this.props.router.params.id))
    }

    componentDidUpdate(prevProps, prevState) {
         /**
         * this is the initial render
         * without a previous prop change
         */
        if(prevProps == undefined) {
            return false
        }

        /**
         * new Project in town ?
         */
        if (this.state.id != this.props.router.params.id) {
           this.props.dispatch(fetchProject(this.props.router.params.id))
           this.setState({id: this.props.router.params.id})
        }

    }

    render() { <Project .../> }
}
chickenchilli
  • 2,911
  • 2
  • 18
  • 23
  • Thanks, this solution works for me. But my eslint setting is complaining about this: https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-update-set-state.md What do you think about this? – konekoya May 10 '17 at 08:29
  • Yeah that's actually not best practice, sorry about that, after dispatching the the "fetchProject" your reducer will update your props anyways, I just used this to make my point without putting redux into place – chickenchilli May 11 '17 at 06:18
  • Hi chickenchilli, I'm Actually still stuck in this... do you have other suggestions or recommended tutorials? Or I'll stick to your solution for now. Thanks for your help! – konekoya May 22 '17 at 07:07
14

If you have:

<Route
   render={(props) => <Component {...props} />}
   path="/project/:projectId/"
/>

In React 16.8 and above, using hooks, you can do:

import React, { useEffect } from "react";
const Component = (props) => {
  useEffect(() => {
    props.fetchResource();
  }, [props.match.params.projectId]);

  return (<div>Layout</div>);
}
export default Component;

In there, you are only triggering a new fetchResource call whenever props.match.params.id changes.

Alfonso Pérez
  • 2,985
  • 2
  • 24
  • 44
  • 4
    This is the better answer given that most of the other answers rely on the now deprecated and insecure `componentWillReceiveProps` – pjb Mar 23 '19 at 12:30
10

Based on answers by @wei, @Breakpoint25 and @PaulusLimma I made this replacement component for the <Route>. This will remount the page when the URL changes, forcing all the components in the page to be created and mounted again, not just re-rendered. All componentDidMount() and all other startup hooks are executed also on the URL change.

The idea is to change components key property when the URL changes and this forces React to re-mount the component.

You can use it as a drop-in replacement for <Route>, for example like this:

<Router>
  <Switch>
    <RemountingRoute path="/item/:id" exact={true} component={ItemPage} />
    <RemountingRoute path="/stuff/:id" exact={true} component={StuffPage} />
  </Switch>
</Router>

The <RemountingRoute> component is defined like this:

export const RemountingRoute = (props) => {
  const {component, ...other} = props
  const Component = component
  return (
    <Route {...other} render={p => <Component key={p.location.pathname + p.location.search}
                                              history={p.history}
                                              location={p.location}
                                              match={p.match} />}
    />)
}

RemountingRoute.propsType = {
  component: PropTypes.object.isRequired
}

This has been tested with React-Router 4.3.

Juha Syrjälä
  • 30,987
  • 31
  • 122
  • 175
5

@wei's answer works great, but in some situations I find it better to not set a key of inner component, but route itself. Also, if path to component is static, but you just want component to remount each time user navigates to it (perhaps to make an api-call at componentDidMount()), it is handy to set location.pathname to key of route. With it route and all it's content gets remounted when location changes.

const MainContent = ({location}) => (
    <Switch>
        <Route exact path='/projects' component={Tasks} key={location.pathname}/>
        <Route exact path='/tasks' component={Projects} key={location.pathname}/>
    </Switch>
);

export default withRouter(MainContent)
Paulus Limma
  • 192
  • 3
  • 8
5

This is how I solved the problem:

This method gets the individual item from the API:

loadConstruction( id ) {
    axios.get('/construction/' + id)
      .then( construction => {
        this.setState({ construction: construction.data })
      })
      .catch( error => {
        console.log('error: ', error);
      })
  }

I call this method from componentDidMount, this method will be called just once, when I load this route for the first time:

componentDidMount() {   
    const id = this.props.match.params.id;
    this.loadConstruction( id )
  }

And from componentWillReceiveProps which will be called since the second time we load same route but different ID, and I call the first method to reload the state and then component will load the new item.

componentWillReceiveProps(nextProps) {
    if (nextProps.match.params.id !== this.props.match.params.id) {
      const id = nextProps.match.params.id
      this.loadConstruction( id );
    }
  }
Oscar Jovanny
  • 599
  • 5
  • 6
0

If you are using Class Component, you can use componentDidUpdate

componentDidMount() {
    const { projectId } = this.props.match.params
    this.GetProject(id); // Get the project when Component gets Mounted
 }

componentDidUpdate(prevProps, prevState) {
    const { projectId } = this.props.match.params

    if (prevState.projetct) { //Ensuring This is not the first call to the server
      if(projectId !== prevProps.match.params.projectId ) {
        this.GetProject(projectId); // Get the new Project when project id Change on Url
      }
    }
 }
  • Are you sure about that? the ComponentDidUpdate is not and is not going to be deprecated in coming version, can you attach some link? the functions to be deprecated as follows: componentWillMount — UNSAFE_componentWillMount componentWillReceiveProps — UNSAFE_componentWillReceiveProps componentWillUpdate — UNSAFE_componentWillUpdate – Angel Mas May 15 '20 at 02:55
0

You can use the method provided:

useEffect(() => {
  // fetch something
}, [props.match.params.id])

when component is guaranteed to re-render after route change then you can pass props as dependency

Not the best way but good enough to handle what you are looking for according to Kent C Dodds: Consider more what you want to have happened.

Vince
  • 556
  • 8
  • 11
0

Here is a pretty simple solution: do a location check in componentDidUpdate, and have a getData function that has the data fetching part with setState:

componentDidUpdate (prevProps) {
    if (prevProps.location.key !== this.props.location.key) {
        this.getData();
    }
}


getData = () => {
    CallSomeAsyncronousServiceToFetchData
        .then(
            response => {
                this.setState({whatever: response.data})
            }
        )
}
R G
  • 21
  • 3
-3

react-router is broken because it must remount components on any location change.

I managed to find a fix for this bug though:

https://github.com/ReactTraining/react-router/issues/1982#issuecomment-275346314

In short (see the link above for full info)

<Router createElement={ (component, props) =>
{
  const { location } = props
  const key = `${location.pathname}${location.search}`
  props = { ...props, key }
  return React.createElement(component, props)
} }/>

This will make it remount on any URL change

catamphetamine
  • 3,245
  • 25
  • 24