Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch is re-mounting the same component on route change #4578

Closed
random-stranger opened this issue Feb 22, 2017 · 42 comments
Closed

Switch is re-mounting the same component on route change #4578

random-stranger opened this issue Feb 22, 2017 · 42 comments
Labels
Milestone

Comments

@random-stranger
Copy link

random-stranger commented Feb 22, 2017

Version

4.0.0-beta.6

Test Case

Same issue as described here, the test case is from it

bug exists
http://codepen.io/ReactJSTraining/pen/ZLggog

no bug, but uses non-supported array as Route path
http://codepen.io/anon/pen/qReegR

Steps to reproduce

UsersIndex component is the one, that is persistent across the routes. Change the route to reproduce the bug (this component is re-mounted).

Expected Behavior

When route is changed, UsersIndex component is not re-mounted, only passed props are changing.

Actual Behavior

When route is changed, UsersIndex component is re-mounted on every route change.

This could be solved with Route path array support or current Switch component fix.

@pshrmn
Copy link
Contributor

pshrmn commented Feb 22, 2017

This might have to do with the usage of React.cloneElement in the <Switch> components render function. I tested this with my <ConfigSwitch>, which uses React.createElement, and it worked as expected (codepen).

@timdorr
Copy link
Member

timdorr commented Feb 22, 2017

Sanity check: Is this actually a problem? Mounting : unmounting !:: construction : destruction.

Switch's goal should be just to juggle which Route is mounted based on the current location. The component is actually instantiated into an element by the render function of the Switch's container. cloneElement is only used as a trick to inject some props into an already instantiated component. Otherwise, we'd just return the appropriate Route, mounting it onto the render tree.

@random-stranger
Copy link
Author

@timdorr it's a common pattern to fetch data in componentDidMount(), it will also trigger on every route change. Same goes for render(), no way to prevent re-rendering.

@timdorr
Copy link
Member

timdorr commented Feb 22, 2017

Sorry, still not getting it (I'm trying!). What's the problem with that? If I've got some user profile page up, navigate away, the user posts some new content, and navigate back to the profile page, I would want to see that new content. So a fresh fetch from the lifecycle method would be good, no?

@vladshcherbin
Copy link
Contributor

vladshcherbin commented Feb 22, 2017

@timdorr in my use-case the component needs to be persistent across several routes.

  • When you navigate to /users, you can see UsersIndex with a list of users.
  • When you navigate to /users/create, you can see the same UsersIndex list of users and UsersCreate form (modal window).

So, when you navigate across this routes, UsersIndex is always visible and has to be mounted and fetched only once. When a new user is added, the users list is automatically updated, no need to re-fetch (from redux).

users

Makes sense now? :)

@timdorr
Copy link
Member

timdorr commented Feb 22, 2017

Sort of. Why are you using a Switch for that? Just have a non-exact Route for the list:

<Route path="/users" component={UserIndex} />
<Route path="/users/create" component={UserCreate} />

That would do the same thing.

@vladshcherbin
Copy link
Contributor

vladshcherbin commented Feb 22, 2017

@timdorr this will render UsersIndex on all /users routes, even on /users/not/found/route.

I want to render NotFound route on all undefined routes.

@vladshcherbin
Copy link
Contributor

vladshcherbin commented Feb 22, 2017

As I mentioned above, Switch can be replaced with an array of paths, passed to Route, but currently it's not supported (but working).

<Route exact path={['/users', '/users/create']} component={UsersIndex} />

@ryanflorence
Copy link
Member

@timdorr I consider this a bug.

@vladshcherbin Please let's not talk about array paths in here, that's a totally different conversation :)

@mjackson
Copy link
Member

Hmm, maybe we don't use cloneElement? In any case, I'd love to see a failing test for this. Makes it a lot easier to fix.

@ryanflorence
Copy link
Member

cloneElement shouldn't cause a remount, I was just looking at the code and I'm a bit stumped

@agundermann
Copy link
Contributor

agundermann commented Feb 23, 2017

It's React.Children.toArray that causes this by setting the keys of the <Route>s.

Returns the children opaque data structure as a flat array with keys assigned to each child. Useful if you want to manipulate collections of children in your render methods, especially if you want to reorder or slice this.props.children before passing it down.

(docs)

Not sure exactly why this was added. If it's needed for flattening or sth, overwriting the key in cloneElement seems to work as well.

Anyway, I think there are valid arguments for both behaviors, both in terms of how you'd initially expect it to work and in terms of what you want to do with it. Though it seems that with the suggested behavior, one could force the remount by themselves, which is not possible the other way around, e.g.:

<Switch>
  <Route render={() => <Comp key="1" />} />
  <Route render={() => <Comp key="2" />} />
</Switch>

@ryanflorence
Copy link
Member

ryanflorence commented Feb 23, 2017

Thanks @taurose your example:

<Switch>
  <Route render={() => <Comp key="1" />} />
  <Route render={() => <Comp key="2" />} />
</Switch>

feels more intuitive to me

@timdorr
Copy link
Member

timdorr commented Feb 23, 2017

Here's a test case:

Hiding the bad test in the HTML...

But I must be doing something wrong, because it passes...

@random-stranger
Copy link
Author

random-stranger commented Feb 23, 2017

@timdorr I guess it is:

ReactDOM.render((
  <Router history={history}>
    <div>
      <Switch>
        <Route exact path="/" component={App} />
        <Route exact path="/foo" component={App} />
      </Switch>
      <Route path="/foo" component={() => <div />}/>
    </div>
  </Router>
), node)

@timdorr
Copy link
Member

timdorr commented Feb 23, 2017

Oh, I'm totally missing things. The App component should not be remounted at all when switching between two component'ed Routes. I thought it was remounting too much just in general (not when going between two unique Routes). My bad...

Let me work that into the test...

@timdorr
Copy link
Member

timdorr commented Feb 23, 2017

OK, here's a failing test:

  it('does not remount a route', () => {
    const node = document.createElement('div')
    const history = createMemoryHistory({ initialEntries: ['/foo'] })

    let mountCount = 0

    class App extends React.Component {
      componentWillMount() { mountCount++ }
      render() { return <div /> }
    }

    ReactDOM.render((
      <Router history={history}>
        <Switch>
          <Route path="/foo" component={App}/>
          <Route path="/bar" component={App}/>
        </Switch>
      </Router>
    ), node)

    expect(mountCount).toBe(1)
    history.push('/bar')
    expect(mountCount).toBe(1)
    history.push('/foo')
    expect(mountCount).toBe(1)
  })

And changing the top of Switch's render to this fixes it:

    const { children:routes } = this.props
    const { location } = this.state

Sorry for being dense earlier!

@timdorr timdorr added the bug label Feb 23, 2017
@timdorr timdorr added this to the v4.0.0 milestone Feb 23, 2017
@mjackson
Copy link
Member

It's React.Children.toArray that causes this

Nailed it, @taurose. Thanks :)

@wmertens
Copy link
Contributor

wmertens commented Mar 3, 2017

@mjackson Can we get a release for this one?

@mjackson
Copy link
Member

mjackson commented Mar 3, 2017

@wmertens Just released this morning in beta 7

@wmertens
Copy link
Contributor

wmertens commented Mar 3, 2017

I am now running beta7 but I'm still seeing my component being remounted. Any quick tips to find the responsible parent?

@vladshcherbin
Copy link
Contributor

vladshcherbin commented Mar 4, 2017

@wmertens it's probably re-rendering due to props change on route change, not re-mounting.

I can confirm there is no bug in beta 7.

Thank you, guys! 👍

@wmertens
Copy link
Contributor

wmertens commented Mar 4, 2017 via email

@wmertens
Copy link
Contributor

wmertens commented Mar 4, 2017

Found it in code of my own (merge mishap). All I had to do was put a breakpoint in Reacts mountComponent and make it conditional on truthy doConstruct. The first component to hit the breakpoint has the culprit parent. Will remember that one, useful.

  mountComponent: function (transaction, hostParent, hostContainerInfo, context) {
[...]
    // Initialize the public class
    var doConstruct = shouldConstruct(Component);
/*=> here*/    var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue);

@VaishaliJain0209
Copy link

I am a bit new to React & React-Router. I am creating a SPA where I do not want components to be unmounted when I switch b/w routes. I am using 4.1.2 version of React-Router but ComponentDidMount is being called every time when I return to a mounted component after switching the route. Could you please suggest the version I should use.

@wmertens
Copy link
Contributor

wmertens commented Aug 11, 2017 via email

@katiepeters
Copy link

This still seems to be a problem if Switch is given an array of Route components (because of the key prop?).

So this doesn't cause a re-mount:

<Switch>
	<Route
		component={App}
		exact
		path="/"
	/>
	<Route
		component={App}
		exact
		path="/other"
	/>
</Switch>

But this does:

<Switch>
	{[
		<Route
			component={App}
			exact
			key="1"
			path="/"
		/>,
		<Route
			component={App}
			exact
			key="2"
			path="/other"
		/>,
	]}
</Switch>

Is that intentional/am I missing something?

@wmertens
Copy link
Contributor

wmertens commented Aug 17, 2017 via email

@pshrmn
Copy link
Contributor

pshrmn commented Aug 17, 2017

@katiepeters Yes, React's reconciler will always unmount the existing component and mount a new one when two element's keys are different.

Edit Woops, I slightly misspoke. This behavior is not actually caused by the reconciler directly. This is the function that determines whether to update the existing component or unmount/mount: https://github.com/facebook/react/blob/master/src/renderers/shared/shared/shouldUpdateReactComponent.js#L25-L43

@katiepeters
Copy link

Ya, I ended up passing them the same key to get it to work. Wasn't sure if that was ideal, but maybe it's ok since Switch only renders one Route at a time

@artywhite
Copy link
Contributor

@wmertens , @pshrmn , @timdorr please take a look on this case with react-router-config here: #5430

@KaroseLiu
Copy link

I'm not sure, this commit fixed the bug, I upgraded the latest version v4.2.0 as your release, and try it, the component also remount.

Here is my layout:

const Layout = ({children, location}) => {
  // url like /:personId/a, /:personId/b
  const personId = location.pathname.split('/')[2]
  // b or a path
  const activeKey = location.pathname.split('/')[3]
  // The problem is every time the path changed, the Profile always componentDidMount.
  return <div>
    <Profile personId={personId}/>
    <Tabs
      type="card"
      activeKey={location.pathname.split('/')[3]}
      onTabClick={(key) => {
        const paths = location.pathname.split('/')
        history.push(`/person/${personId}/${key}`)
      }}
    >
      {tabs.map(tab =>
        (<TabPane tab={tab.title} key={tab.key}>
          {children}
        </TabPane>),
      )}
    </Tabs>
  </div>
}

The below is the Router config:

const routes = [
  {
    path: '/a',
    component: A,
  },
  {
    path: '/b',
    component: B,
  },
  {
    path: '/c',
    component: C,
  },
]
const Router = ({ match }) => (
  <Switch>
    {
      routes.map(({ path, exact, strict, component: Comp }) =>
        (<Route
          key={path}
          path={`${match.url}${path}`}
          exact={exact}
          strict={strict}
          render={
            props => (
              <Layout {...props}>
                <Comp {...props} />
              </Layout>
            )
          }
        />),
      )
    }
  </Switch>
)

Every time the path change, the component will didMount again, so maybe my usage is the incorrect way to reuse the layout?

@TimoRuetten
Copy link

TimoRuetten commented Oct 5, 2017

I am causing the same problem. Any updates to this ?

    <Switch>
      <Route
        path="/:param"
        component={App}
      />
      <Route
        path="/:param/level-2"
        component={App}
      />
      <Route
        path="/:param/level-2/level-3"
        component={App}
      />
    </Switch>

This code will causes always a remount (not update of Component - I am talking about a remount) so everytime the url changed the componentWillMount get fired.

@wmertens
Copy link
Contributor

wmertens commented Oct 5, 2017

@TimoRuetten that is very weird, can you share your whole code? This one looks too simplified…

@TimoRuetten
Copy link

TimoRuetten commented Oct 5, 2017

@wmertens Yeah - its a bit simplified - but at the end not that much. This is the current code of our <AppRouter /> which is the first rendered Main-Component after all Provider we are using:

export default () => (
  <div>
    <Switch>
      <Route
        path="/:langCode"
        component={App}
      />
      <Route
        path="/:langCode/p/:nodeSlug/:nodeId"
        component={App}
      />
      <Route
        path="/:langCode/l/:regions*"
        component={App}
      />
    </Switch>
  </div>
);

Then we have in our <App /> a simple log in componentWillMount which gets always fired when clicking on a <Link /> link.

edit Using version 4.2.0

@wmertens
Copy link
Contributor

wmertens commented Oct 5, 2017

Are you sure it's the Switch and not something higher up? You can also put ?react_perf in your url and then you can use a Performance recording to see unmounts in the user events

@TimoRuetten
Copy link

Hey @wmertens - You were right, I'm sorry. A HOC of my <App /> produced this problem! Sorry for the confusion and thanks for the fast response & help!

@wmertens
Copy link
Contributor

wmertens commented Oct 5, 2017

@KaroseLiu you should give your Routes all the same key…

@julienvincent
Copy link

julienvincent commented Oct 7, 2017

I still seem to be getting this issue (v4.2.2). I have confirmed that I have no HOC's or anything else affecting my app:

class T extends Component {
  componentWillUnmount() {
    console.log("unmounting")
  }

  render() {
    return null
  }
}

...

<BrowserRouter>
  <div>
    <Switch>
      <Route path="/a" key="1" /*or no key*/ children={() => <T />}
    </Switch>
  </div>
</BrowserRouter>

Updating the path results in unmounting getting logged

@agundermann
Copy link
Contributor

This snippet works as expected without any unnecessary remounts, so the problem must be somewhere else.

@theenoahmason
Copy link

theenoahmason commented Oct 11, 2017

for anyone searching here, I am not getting remounts on switch, UNLESS switch is wrapped with react transition group's TransitionGroup/CSSTransition components.

correction, I believe my above comment is only an issue when passing a location to switch, which react transition group requires.

It seems animating a switch with react transition group requires a location for the switch component, which in turn is causing a remount on the route's called component.

@wmertens
Copy link
Contributor

wmertens commented Oct 11, 2017 via email

@remix-run remix-run locked and limited conversation to collaborators Nov 17, 2017
@remix-run remix-run deleted a comment from hainessss Nov 17, 2017
sbyps referenced this issue in KaroseLiu/planet Dec 7, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests