Search code examples
reactjsreact-routerreact-motion

Nested Match routes with React Router v4 and MatchWithFade


This question is a follow-up to this:

Trouble with React Router v4 and MatchWithFade

I've got another (potentially silly) question about using MatchWithFade and React Router v4. What I'd like to do is have nested routes, so a top-level component might have this:

<MatchWithFade pattern='/one' component={One} />

...and then One might have this:

<Match pattern='/one/one' Component={OneOne} />

This doesn't strike me as an unusual pattern (though maybe it is). In any event, the behavior I'm observing is that, using the above example, if I load OneOne, it gets mounted, and then componentWillUnmount is immediately called. Had I to guess, I'd say that TransitionMotion is keeping track of a (perhaps hidden) instance of OneOne, and once the transition is complete, it unmounts that hidden component. As far as the basic UI is concerned, OneOne is rendered. However, if componentWillUnmount does any cleanup (like, say, removing something from Redux), then of course that action is fired, and any data tied to OneOne is blown away.

Here's a complete example that illustrates the problem:

import React, { Component } from 'react';
import BrowserRouter from 'react-router/BrowserRouter'

import { TransitionMotion, spring } from 'react-motion'
import Match from 'react-router/Match'
import Link from 'react-router/Link';

const styles = {
  fill: { position: 'absolute', top: 0, left: 0 }
};

const MatchWithFade = ({ component:Component, ...rest }) => {
  const willLeave = () => ({ zIndex: 1, opacity: spring(0) })

  return (
    <Match {...rest} children={({ matched, ...props }) => {
      return (
        <TransitionMotion
          willLeave={willLeave}
          styles={matched ? [ {
            key: props.location.pathname,
            style: { opacity: 1 },
            data: props
          } ] : []}
        >
          {interpolatedStyles => {
            return (
              <div>
                {interpolatedStyles.map(config => (
                  <div
                    key={config.key}
                    style={{...styles.fill, ...config.style }}>
                    <Component {...config.data}/>
                  </div>
                ))}
              </div>
            )
          }}
        </TransitionMotion>
      )
    }}/>
  )
}

const TwoOne = () => {
  return (
    <div>Two One</div>
  )
}


class TwoTwo extends Component {
  componentWillUnmount() {
    console.log("TwoTwo will unmount")
  }

  render () {
    return (
      <div>Two Two</div>
    )
  }
}


const TwoHome = () => {
  return (
    <div>Two Home</div>
  )
}


class One extends Component {
  componentWillUnmount () {
    console.log("ONE UNMOUNTING")
  }
  render () {
    return (
      <div style={{ width: 300, border: '1px solid black', backgroundColor: 'orange', minHeight: 200}}>
        One one one one one one one one one one<br />
        One one one one one one one one one one<br />
      </div>
    )
  }
}

const Two = () => {
  return (
    <div style={{ width: 300, border: '1px solid black', backgroundColor: 'yellow', minHeight: 200}}>
      <Match pattern='/two/one' component={TwoOne} />
      <Match pattern='/two/two' component={TwoTwo} />
      <Match pattern='/two(/)?' exactly={true} component={TwoHome} />
    </div>
  )
}


class App extends Component {

  render () {
    return (
        <BrowserRouter>
          <div style={{padding: 12}}>
            <div style={{marginBottom: 12}}>
              <Link to='/one'>One</Link> || <Link to='/two'>Two</Link>
              || <Link to='/two/one'>Two One</Link>
              || <Link to='/two/two'>Two Two</Link>
            </div>
            <div style={{position: 'relative'}}>
              <MatchWithFade pattern='/one' component={One} />
              <MatchWithFade pattern='/two' component={Two} />
            </div>
          </div>
        </BrowserRouter>
    )
  }
}

export default App;

If you load this and open a console, toggle between the One and Two links. You'll see the cross fade happen in the UI, and you'll see "ONE UNMOUNTING" in the console when the transition from One to Two completes. So that's right.

Now, click between Two One and Two Two. In this case, when Two One is clicked, you'll immediately see "TwoTwo will unmount" in the console, which is good. However, if you click Two Two, you'll see "TwoTwo will unmount" after about a second--which I take to be the amount of time the parent MatchWithFade takes to execute.

So I'm not sure what's going on here. Is my code just busted? Am I doing something that RRv4 cannot support? Have I uncovered a bug?

Any help/guidance is appreciated!


Solution

  • Your issue is your use of props.location.pathname as a key. This should always be the same for a component, but the way that you have written it, it changes each time that you navigate. Try changing this:

    const styles = {
      fill: { position: 'absolute', top: 0, left: 0 }
    };
    

    to:

    const styles = {
      fill: { position: 'relative', top: 0, left: 0 }
    };
    

    and you will see that you are rendering two instances of <Two> (one for each key).

    If you were to use a constant key, such as rest.pattern (the pattern associated with this <Match>), your issue would go away.

    styles={matched ? [ {
      key: rest.pattern,
      style: { opacity: 1 },
      data: props
    } ] : []}