remix-run / react-router

Declarative routing for React
https://reactrouter.com
MIT License
53.16k stars 10.31k forks source link

More practical animation example #3936

Closed stnwk closed 8 years ago

stnwk commented 8 years ago

First of all: Thanks for the great new release! 😊

I am already heavily using it and look for a way to transition between routes. To be more precise:

How do I keep the children of one route still on screen while I prefetch data for rendering the following route after a click?

A short code-example would be nice, as the docs are still pretty fresh and don't include it. Thanks!

timdorr commented 8 years ago

There's an example right in the docs: https://react-router.now.sh/animated-transitions

stnwk commented 8 years ago

No, @timdorr. The example does not at all answer my question? It uses React-Motion to transition background-color, but it doesn't show how to keep the old route still on screen, although the new one was already triggered.

Do you see what I mean? It's not a very good example I think, doesn't give much practical use.

timdorr commented 8 years ago

OK, that's a fair assessment. Perhaps we can better show an example where we wrap a <Match> with something that keeps it's children rendered for a set period of time (sort of like with <ReactCSSTransitionGroup>).

stnwk commented 8 years ago

Yes, that's more of what I'm looking for. I think that'll be a good idea, as it's much more realistic use-case - rather than using react-motion.

HofmannZ commented 8 years ago

I'm also trying to figure out how to get my page transitions going, it's nothing like v2/v3 😕

stnwk commented 8 years ago

So, this is how I managed to use ReactTransitionGroup react-addons-transition-group (see React docs) with React Router v4:

App.js

import React from 'react';
import Router from 'react-router/BrowserRouter';
import Match from 'react-router/Match';
import Link from 'react-router/Link';

// new
import AnimatedMatch from './AnimatedMatch';

const App = () => (
  <Router>
    <div>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/topics">Topics</Link></li>
      </ul>

      <hr/>

      <AnimatedMatch exactly pattern="/" component={Home} />
      <AnimatedMatch pattern="/about" component={About} />
      <AnimatedMatch pattern="/topics" component={Topics} />
    </div>
  </Router>
);

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);

const About = () => (
  <div>
    <h2>About</h2>
  </div>
);

const Topics = () => (
  <div>
    <h2>Topics</h2>
  </div>
)

This is basically the Basic Example from the current docs, i only just replaced the Match Component with a custom AnimatedMatch component.

AnimatedMatch.js

import React, { Component } from 'react';
import Match from 'react-router/Match';
import TransitionGroup from 'react-addons-transition-group'; // here comes the magic
import Page from './Page'; // new

export default class AnimatedMatch extends Component {
  render() {
    const data = this.props;

    return (<Match
      {...this.props}
      children={({ matched, ...props }) => {
        return (<TransitionGroup>
          {matched && <Page>
            <data.component {...props} />
          </Page>}
        </TransitionGroup>);
      }}
    />);
  }
}

Here I basically render the React-Router v4 Match Component and make use of the children prop to wrap our actual desired component data.component (in that case) with a TransitionGroup to set up the component with hooks to animate the underlying DOM. For that matter we will need another component where we can define those hooks Page.js.

Page.js

import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';

export default class Page extends Component {
  componentWillEnter(cb) {
    // animate stuff, then call cb();
    const element = findDOMNode(this);
    cb();
  }

  componentWillLeave(cb) {
    // animate stuff, then call cb();
    const element = findDOMNode(this);
    cb();
  }

  render() {
    return this.props.children;
  }
}

Here I only render the children which is actually our desired component from the Match and set up hooks like componentWillEnter and componentWillLeave. For more infos on this API take a look at the official react docs React Transition Group.

I don't really have the time to make a full PR, but I think this pretty much solves it and is a good example for some practical usage.

Hope it helps @HofmannZ :)

HofmannZ commented 8 years ago

Hmm I implement the same thing for the Miss component but it does not seem to work as aspected: the AnimatedMiss file in my GitHub repo

While the implementation for Match works just fine: the AnimatedMatch file in my GitHub repo

stnwk commented 8 years ago

Okay, @HofmannZ so you can't currently use lifecycle hooks to animate a <Miss> component. The <Miss> component does currently not offer a children prop which is needed in order to use lifecycles for animation.

I opened a new feature-request issue #4026 and explained the reason there too.

gabrielbull commented 8 years ago

This could also be used to create a pinterest style modal #4029 . The docs could benefit from having a modal example as well.

jackismissing commented 8 years ago

Thanks @stnwk for your snippet! I was looking for a way to animate simple page transitions with react-router and transition groups and it did the trick. :ok_hand:

ryanflorence commented 8 years ago

<MatchGroup> will also make animation with transition group simpler

ryanflorence commented 8 years ago

cf2555d9031c02780b1324d1880ddc4f5953ed71

example update coming soon, but basically you can do:

<CSSTransitionGroup {..whatever}>
  <MatchGroup>
    <Match pattern="/foo" component={Foo}/>
    <Match pattern="/bar" component={Bar}/>
    <Miss component={Nope}/>
  </MatchGroup>
</CSSTransitionGroup>

Only one child will ever render in there, so it'll work like all other transition group usage.

hopglascock commented 8 years ago

@ryanflorence I don't think it will...

The Transition Group is only able to track changes and call events on its direct children.

You would need to do something like this

<MatchGroup WrapperComponent={TransitionGroup}>
   <Match pattern="/foo" component={Foo}/>
   <Miss component={Nope}/>
</MatchGroup>

Then in MatchGroup

render() {
  const { children } = this.props
  return (
  <LocationSubscriber>
    {(location) => {
      const { matchedIndex, missIndex } = this.findMatch(location)
      return (
        <WrapperComponent>
          { matchedIndex != null ? (
              children[matchedIndex]
            ) : missIndex ? (
              children[missIndex]
            ) : null }
        <WrapperComponent>
      );
    }}
  </LocationSubscriber>
  )
}

Something like that??

Point being Transition Groups need to be directly around the matched components. I would very much like to use Transition Groups with MatchGroups.

ro-savage commented 7 years ago

I struggled with understand how this would work for my switch routing. Eventually found a simple solution for what I was doing.

Which was basically wrapping my <Switch> in a <Route> that receives a location and key.

        <BrowserRouter>
          <div>
            <Route component={TopMenu} />
            <Route render={({ location }) => (
              <CSSTransitionGroup transitionName="fade" transitionEnterTimeout={500} transitionLeaveTimeout={500}>
                <Route location={location} key={location.key}>
                  <Switch>
                    <Route exact path="/" component={HomePage} />
                    <Route path="/tickets" component={TicketsPage} />
                    <Route path="/event/:id" component={(params) => {
                        return <EventPage id={params.match.params.id} />
                      }}/>
                    <Route path="/search" component={(params) => {
                        return <SearchResultsPage queryString={params.location.search} />
                      }}/>
                    <Route path="*" component={FourOhFourPage} />
                  </Switch>
                </Route>
              </CSSTransitionGroup>
              )}
            />
          </div>
        </BrowserRouter>
Findiglay commented 7 years ago

@ro-savage Thanks this works for me. This is currently the only working example I can find for v4.0 + CSSTransitionGroup

ghost commented 7 years ago

@ro-savage It hasn't worked for me yet.

ro-savage commented 7 years ago

@andersonsousa if you are following my example, and the routes are working but not the transitions, you might find that you haven't implemented CSSTransitionGroup correctly. E.g. make sure you have created the correct classes, in my example fade classes

.fade-enter {
  opacity: 0.5;
  z-index: 1;
}

.fade-enter.fade-enter-active {
  opacity: 1;
  transition: opacity 500ms ease-in;
}

If that doesn't solve the issue, you'll have to just keep playing. It works for me and I assume all the people who left emojis.

ghost commented 7 years ago

@ro-savage Actually I had forgot the order of components.

Route > CssTransition > Route > Switch > { Routes }

I had forgot this first Route which renders the CssTransition. Now its fully working. I only have one question. Is there any way to not animate some Routes?

uxlayouts commented 7 years ago

What about if you use react-router-config?

I have tried: react-easy-transition throws propType errors and new page loads before old page can disappear react-router-transition Position absolute is pretty gross when components collapse react-css-transition-replace Looks promising but I can't get the structure right. (Used in example below)

I would like the cross fade with animated height Demo Here: react-css-transition-replace Demo

Related RR4 + react-css-transition-replace Thread: https://github.com/marnusw/react-css-transition-replace/issues/46

Here is my code:

App.js page:

import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import routes from './routes';

const App = () => {
  return (
    <Router>
      {renderRoutes(routes)}
    </Router>
  );
}

export default App;

Base.js page (Base class inside App.js):

import React from 'react';
import { renderRoutes } from 'react-router-config';
import ReactCSSTransitionReplace from 'react-css-transition-replace';
import { shape } from 'prop-types';
import Header from './Header';
import './Base.css';
import './styles/includes.css';

const Base = ({route, location}, {router}) => (
  <div className="App">
    <Header />
    <ReactCSSTransitionReplace
      transitionName="cross-fade"
      transitionEnterTimeout={1000}
      transitionLeaveTimeout={1000}
    >
      {renderRoutes(route.routes, { key: location.pathname,})}
    </ReactCSSTransitionReplace>
  </div>
);

Base.propTypes = {
  location: shape({}),
};

export default Base;")

And by routes.js page:

import Base from './Base';
import Home from './Home';
import About from './About';
import NotFound from './NotFound';
import Change from './Change';
import OurTeam from './OurTeam';
import Tacos from './Tacos';
import Chicken from './Chicken';
import Veggie from './Veggie';
import Posts from './Posts';
import Post from './Post';

export default [
  { component: Base,
    routes: [
      { path: '/',
        exact: true,
        component: Home,
      },
      {
        path: '/about',
        component: About,
        name: 'About',
      },
      {
        path: '/change',
        component: Change,
        name: 'Change',
      },
      {
        path: '/our-team',
        component: OurTeam,
        name: 'OurTeam',
      },
      {
        path: '/tacos',
        component: Tacos,
        routes: [
          { path: '/tacos/chicken',
            component: Chicken,
          },
          { path: '/tacos/veggie',
            component: Veggie,
          }
        ]
      },
      {
        path: '/posts',
        exact: true,
        component: Posts,
        name: 'Posts',
      },
      {
        path: '/posts/:id',
        component: Post
      },
      {
        component: NotFound,
      },
    ]
  },
]

Css file:

.cross-fade-leave {
  opacity: 1;
}
.cross-fade-leave.cross-fade-leave-active {
  opacity: 0;
  transition: opacity 1s ease-in;
}

.cross-fade-enter {
  opacity: 0;
}
.cross-fade-enter.cross-fade-enter-active {
  opacity: 1;
  transition: opacity 1s ease-in;
}

.cross-fade-height {
  transition: height .5s ease-in-out;
}
romanlex commented 7 years ago

@uxlayouts your example not work. renderRoutes receive only one argument - its routes. How fix it?

AntonioRedondo commented 7 years ago

@ro-savage above example works. But it hasn't been explained yet some other example where the component animation overlaps with the former component. It is to say, both components remain on screen for a short time, usually with the position: absolute CSS property set and some opacity and transform transition involved.

In order to achieve such transition you'll have to handle the routes yourself. Don't have more than one <Route> on the <Router>. If there are more than one <Route> the component associated for that route will be immediately unmounted without waiting for the transition group time out. So instead, we'll have just one <Route> and we'll control when to show and hide components associated to different routes.

For example, using react-transition-group 2.x (please note the syntax and usage has changed from version 1.x), on a RandomComponent.jsx:

// Other imports...
import { TransitionGroup, CSSTransition } from "react-transition-group";

function getPage(pathname) {
    let component;

    switch (pathname) {
        case "/": component = <Component1/>; break;
        case "/path2": component = <Component2/>; break;
        case "/path3": component = <Component3/>;
    }

    return (
        <CSSTransition
                key={ pathname } // If you don't set a key the animation won't work
                timeout={ 500 }
                classNames="animation-classes">
            { component }
        </CSSTransition>
    );
}

export default function RandomComponent({ location }) {
    return (
        <TransitionGroup>
            { getPage(location.pathname) }
        </TransitionGroup>
    );
}

The CSS for the transitions:

.animation-classes {
    &-enter {
        position: absolute;
        opacity: 0;
        transform: translate3d(8%, 0, 0);
    }

    &-enter&-enter-active {
        z-index: 1;
        opacity: 1;
        transform: translate3d(0, 0, 0);
        transition: 500ms;
    }

    &-exit {
        opacity: 1;
    }

    &-exit&-exit-active {
        opacity: 0;
        transition: 500ms;
    }
}

Then on the entry point index.jsx:

<Router>
    <Route path="/" component={ MyRandomComponent } /> // No more than one route
</Router>

See the final result here.

liushigit commented 7 years ago

@stnwk Is Match deprecated? I didn’t find it in the current v4.

stnwk commented 7 years ago

@liushigit Yes, they changed the API over time. My code example is not api conform anymore. I'm sorry!

Please check out later comments or the official docs for a more recent implementation.