Skip to content
This repository was archived by the owner on Dec 15, 2018. It is now read-only.

ReactCSSTransitionGroup support for route transition animations #117

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

michaelhogg
Copy link
Contributor

It would be great to be able to perform route transition animations using ReactCSSTransitionGroup.

Especially for hybrid mobile apps, animated route transitions are an important feature for a router.  Some examples:

Initial attempt

function Root(props) {
    return (
        <div>
            <div>
                <Link href="/first-page">First</Link> | { }
                <Link href="/second-page">Second</Link>
            </div>
            <ReactCSSTransitionGroup transitionName="example"
                                     transitionEnterTimeout={1000}
                                     transitionLeaveTimeout={1000}>
                <RelativeFragment forRoute="/first-page">
                    <div>This is the first page</div>
                </RelativeFragment>
                <RelativeFragment forRoute="/second-page">
                    <div>Here is the second page</div>
                </RelativeFragment>
            </ReactCSSTransitionGroup>
        </div>
    );
}

This doesn't seem to work.

Rendered output

Here's the rendered output for the /first-page route (the <span> elements are rendered by ReactCSSTransitionGroup):

<div data-reactroot="">
    <div> ... Links ... </div>
    <span>
        <div>
            <div>This is the first page</div>
        </div>
        <!-- react-empty: 9 -->
    </span>
</div>

and for the /second-page route:

<div data-reactroot="">
    <div> ... Links ... </div>
    <span>
        <!-- react-empty: 10 -->
        <div>
            <div>Here is the second page</div>
        </div>
    </span>
</div>

The problem

I think the ReactCSSTransitionGroup fails to animate its RelativeFragment children on route changes because the RelativeFragment children are not actually being added or removed; they're just changing their rendered output.

When the Fragment component renders null for non-matching routes, I believe the RelativeFragment component gets rendered as a ReactDOMEmptyComponent (notice the react-empty elements above).

For ReactCSSTransitionGroup animations to work, I believe its child fragments must actually be removed for non-matching routes (rather than being replaced by ReactDOMEmptyComponents).  I don't think this is possible with the current functionality of Redux Little Router.

A possible solution

This pull request is an experiment to make it possible to use ReactCSSTransitionGroup.

My idea is to use a new component which sits in-between the low-level Fragment and the high-level AbsoluteFragment/RelativeFragment.  I've called it the WrappedFragment.  It does the following:

  • Obtains the rendered output from Fragment (which will either be null or a React element).
  • Wraps it directly with a user-specified Wrapper component (which can be a ReactCSSTransitionGroup or any other suitable component).  This is what enables ReactCSSTransitionGroup to work correctly.  When the rendered output from Fragment becomes null, ReactCSSTransitionGroup will correctly perform the "leave" animation.  See the React docs: "Animating One or Zero Items".

The user-specified Wrapper component is supplied via an optional wrapper prop.  If the user doesn't supply that prop, then by default the WrappedFragment component will simply render the Fragment without any wrapping (just as if it had been rendered directly by AbsoluteFragment or RelativeFragment).

Example usage

First we create our Wrapper component:

function Wrapper(props) {
    return (
        <ReactCSSTransitionGroup transitionName="example"
                                 transitionEnterTimeout={1000}
                                 transitionLeaveTimeout={1000}>
            {props.children}
        </ReactCSSTransitionGroup>
    );
}

Then we pass our Wrapper component in the wrapper prop of the RelativeFragments:

function Root(props) {
    return (
        <div>
            <div>
                <Link href="/first-page">First</Link> | { }
                <Link href="/second-page">Second</Link>
            </div>
            <RelativeFragment forRoute="/first-page" wrapper={Wrapper}>
                <div>This is the first page</div>
            </RelativeFragment>
            <RelativeFragment forRoute="/second-page" wrapper={Wrapper}>
                <div>Here is the second page</div>
            </RelativeFragment>
        </div>
    );
}

Example rendered output

Here's the rendered output for the /first-page route (again, the <span> elements are rendered by ReactCSSTransitionGroup):

<div data-reactroot="">
    <div> ... Links ... </div>
    <span>
        <div>
            <div>This is the first page</div>
        </div>
    </span>
    <span></span>
</div>

and during a transition from /first-page to /second-page:

<div data-reactroot="">
    <div> ... Links ... </div>
    <span>
        <div class="example-leave example-leave-active">
            <div>This is the first page</div>
        </div>
    </span>
    <span>
        <div class="example-enter example-enter-active">
            <div>Here is the second page</div>
        </div>
    </span>
</div>

and once the transition has completed:

<div data-reactroot="">
    <div> ... Links ... </div>
    <span></span>
    <span>
        <div>
            <div>Here is the second page</div>
        </div>
    </span>
</div>

You can see that ReactCSSTransitionGroup correctly applies the example-leave example-leave-active CSS classes to the outgoing fragment, and the example-enter example-enter-active classes to the incoming fragment. 🎉

Feedback/discussion appreciated!

This is a non-breaking (backwards-compatible) change to Redux Little Router's API (the new wrapper prop on AbsoluteFragment and RelativeFragment is optional).

This may not be the best way to make ReactCSSTransitionGroup work with Redux Little Router, and so I haven't updated the README or Mocha tests yet, but I wanted to submit this pull request as a starting point for the discussion.

Some questions:

  1. I couldn't figure out a way to easily integrate ReactCSSTransitionGroup with Redux Little Router's current API.  Does anyone know if this is possible?  If so, then this pull request may not be necessary :)
  2. I chose "wrapper" as a generic name for the new functionality, because I thought there could be other uses-cases besides ReactCSSTransitionGroup in which a user might want to wrap a fragment's contents in a custom component.  Does this seem reasonable?  If not, should the "wrapper" functionality be renamed to something more specific like "transition"?

Many thanks to @tptee and everyone else for creating such a great router ☀️

@codecov-io
Copy link

codecov-io commented Oct 28, 2016

Current coverage is 97.04% (diff: 60.00%)

Merging #117 into master will decrease coverage by 1.55%

@@             master       #117   diff @@
==========================================
  Files            18         19     +1   
  Lines           215        237    +22   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits            212        230    +18   
- Misses            3          7     +4   
  Partials          0          0          

Powered by Codecov. Last update d748f58...43c9f97

@michaeljonathanblack
Copy link
Contributor

I'd love to see ReactCSSTransitionGroup supported, although this solution feels a little like it's hacking around the corners of a better approach.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants