Advanced route transitions with React Transition Group and Popmotion [Part 1]

This tutorial will show how to replicate the App Store transition animation using React Transition Group and the Popmotion animation library. Part 1 focuses on bootstrapping the app.

David Bismut
15 min readApr 9, 2018
What we will be building!

Deprecation note

This tutorial was written more than a year ago. Since then, a lot has happened, Popmotion is now Framer Motion and offer more features. In the meantime, I decided to recreate the same prototype, using react-spring and react-use-gesture. The full commented code is available on codesandbox, and if you’re on a mobile you might want to try the full screen demo. The code follows some of the ideas explained here, but the setup has been simplified, and there’s now an iPad mode.

If you’re not interested in setting up routers and redux stores, skip over to Part 2.

Disclaimer

I’m no pro developer: although I work with developers, I’m no developer myself, and most of what I know is self-taught on my spare time. You won’t learn any code best practices (functional programming, HOC, performance tweaks), and if you look at the code you might find redundancies, errors or even worst practices. If so, please leave a comment.

I’m not a native English speaker: apologies in advance for all grammatical mistakes and clumsy syntax.

Code isn’t thoroughly tested: the code has been tested on an iPhone X running iOS 11.3 and on a Macbook Pro with Chrome 66. Unfortunately, some glitches remain on Safari desktop.

Copyright: for design purposes, I’ve been using illustrations I’ve shamelessly taken from Pinterest. If this causes any issues let me know and I’ll change them.

This series of posts highlight the basics for replicating – in a browser – the animation of the new App Store Today tab when moving from the article list to an article page. However, I won’t be focusing exclusively on how to mimic the animation itself but rather how to make this transition work in the context of a full packaged app with state management and routing, with the list and article pages having their own distinct URLs.

Also, I wanted the scroll on the iPhone to behave natively, with Safari address and tab bars retracting when scrolling down like they would on a normal web page. This may not look like a big deal, but I found this sole functionality to add major complexity as it requires clipping divs and manually resetting scroll positions. But we’ll come to this later.

All in all, our objective might not seem like an overly complex to achieve, but perfecting it was a long effort, looking at numerous StackOverflow and Github issues. So this is a comprehensive tutorial so that everything works great with no glitches. Hopefully someone will find this useful :)

If you don’t want to follow the tutorial and just want to see the prototype in action, click here. It’s going to be a long read, so if you want to make sure this is worth your time, I encourage you to have a look.

Main packages we will be using

  • Yarn: a faster alternative to node packet manager npm.
  • The now famous create-react-app environment that helps setting up a feature-loaded React environment in no time.
  • Sass for CSS styling (I suck at Sass so don’t expect to learn anything regarding Sass here).
  • Router5: a universal and agnostic routing solution that I prefer over ReactRouter as it doesn’t mix with your Component tree.
  • Redux for state management (but you won’t be learning much about Redux in this post).
  • React Transition Group will let us animate components entering and leaving the DOM.
  • Popmotion, a fast javascript DOM animation library.
  • Pressure.js: very cool little library that handles Force Touch or 3D touch in the browser.

Note that Popmotion just released a React Pose library with a PoseGroup component that could replace TransitionGroup and possibly make the following code more declarative. I’ll have a look in the coming days and update this tutorial if necessary.

Animation analysis

Let’s start with our design goal. The App Store animation is — as usually with Apple, neatly crafted: easings and timings are perfect, everything reacts smoothly under the finger. We’re not going to reach the same level of perfection today, but we’ll try to get as close as we can.

Entering Transition

We’ll be calling entering transition the transition from the list page to the article page, and exiting transition the reverse transition from the article page to the list page.

App Store entering transition

The entering transition can be decomposed as below:

  • the user taps the article preview in the list page.
  • the article preview subtly bounces in…
  • …before expanding full screen. Round borders become sharp and the content of the article seems to be overflowing from under the cover.

There are actually numerous more details in the App Store animation that we won’t be addressing in this article, such as the cover borders equally becoming sharp.

Exiting Transition

The exiting transition is slightly more complex to implement, as it is twofolds: tapping the close icon – or scrolling up to the point where the article page becomes draggable and can be released.

On the left, tapping the closing icon animation, and on the right, the dragging to close animation

Tapping the close icon reverses the entering transition (with a spring easing) while at the same time scrolling the article up to the top. Dragging or actually scrolling up the article beyond its upper limit scales it down and reveals the list page blurred in the background. After a certain threshold, the article bounces back into the list. We’ll try to implement all of this in our app.

What we will be building in comparison.

Agenda

This tutorial is going to be split in three parts:

  • Part 1 (this part) focuses on setting up the app and its core routing mechanics and connecting the redux store. It also implements the basic components that we will use later on.
  • Part 2 will get more interesting as it describes how to delegate the transition animation to the Popmotion library. We will cover the entering and exiting animations.
  • Part 3 will show how to add the drag to close feature, and we’ll play with the Pressure.js library just to make things more complicated.

Getting started

It’s going to be a long run with a lots of files to create, so please be patient!

Setting up Create React App with Sass

I won’t get into much details about installing Create React App and creating an app. Just follow the instructions on the create-react-app project page and if you want to be running Sass, make sure you follow those steps.

Creating the Router and Store

As I’ve said before, the idea is to package the animation within a real-app environment, with routing and state management. There are numerous guides online that explain all this much better than I would do so I won’t spend much time on detailing the set up. But since Router5 is not as popular as ReactRouter yet, let me drive you through a few steps on how to make it work.

Let’s first add Redux and Router5 packages with their React / Redux relative dependencies:

yarn add redux react-redux redux-logger router5 react-router5 redux-router5 router5-transition-path

Then create two directories:

  • ./src/_redux that will contain the files needed to set up the redux store.
  • ./src/_router that will contain the files needed to set up the Router5 router and routes.

Let’s start with the router part.

Defining our routes

For the purpose of this prototype, we will be using local data. Create a file named postData.js in ./src/_router and paste this gist inside it.

We will only have two routes, one for the list page, and one for the article page. Create a routes.js file inside the ./src/_router directory, and paste the content below:

The home route will return all the posts (minus the content attribute that isn’t needed on the home page), while the post route will return only the data related to the post matching the id passed as a parameter in the URL (i.e. http://localhost:3000/post/3).

The onActivate attribute is a function that we will call from a middleware we will create it later as explained in the docs. onActivate is responsible for retrieving the data needed to display each route. Even though the data is stored locally in ./src/_router/postData.js, I’ve returned it as a Promise so that you can actually replace it with a fetch call querying a distant API without changing much code.

The data fetching middleware

The data middleware will take care of triggering the route transition once the route data has been fully fetched.

The middleware is executed every time the router changes from one route to another. It returns an object that will be used as the state of the next route.

Create a new file ./src/_router/dataMiddleware.js and paste the code below.

Essentially our middleware runs through all the routes that should be activated (in our case only one since we don’t have nested routes), and stores all onActivate functions from our route into an array of Promises (remember we’ve written onActivate to return a Promise).

Then the middleware waits for all these Promises to resolve (that’s the Promise.all bit), reduces all the Promises results into one object, and adds this object as the data attribute of toState. Only when Promise.all resolves and returns the new toState object will the transition between the routes be finalized and dispatched to our redux store (that we haven’t created yet, this will come next). And only then will our transition animation start.

Note that the onActivate function, the data attribute of toState can be labeled to whatever you want, they are not following any API. This is also why Router5 is so great.

I know this may look overly complicated considering that we’re fetching local data and that we only have one Promise to resolve, but I had a hard time myself figuring out how this worked the first time, so I though it might be worth including this part here.

Creating the router

Now let’s create the router in ./src/_router/router.js with the code below.

We’re just copy pasting the boiler plate from Router5 documentation. Just one note though: scrollRestoration is an attribute that was recently added to window.history and that handles the scroll position of the page when you navigate from one URL to another (check the specifications for further information). We’re setting the scroll restoration to manual since we want to be in full control of how scroll is handled in our app.

Setting up Redux

I’ll be quick on this one. If you’re not familiar with redux, reducers or actions, maybe you should start with the awesome Redux documentation. If you just want to move on to the motion part, just copy paste the code in the right files and skip the explanations.

First, we’ll create a file in ./src/_redux/actions/index.js that will combine all our reducers. We won’t be creating any custom reducers or actions for this prototype, so we only combine the router5 reducer that will add the router object to our app state:

Then, create a file in ./src/_redux/store.js which exports a function that creates our store, adds the reducer we’ve just created and the router5 middleware. Again, I’m just copying elements from router5 official documentation.

Connecting the dots

Finally, we need to connect everything together. To do so, locate the index.js file at the root of the ./src directory and replace it with the content below:

There’s really nothing exotic there. We’re creating the router with everything we’ve seen before, wrapping our app inside Redux and Router5 providers and starting the router.

Let’s start the app!

Type yarn start in the terminal and wait for create-react-app to compile your code and start the web server. Your browser should open and, well… there’s nothing different from the default create-react-app boilerplate.

However, if you open your browser console, you should see that everything is working as expected, and that our route of the app state has the data it needs for our page to display our list!

The Chrome Inspector after our app loads showing logs from redux-logger.

The front-end

Let’s start with the different components that make the front-end structure:

  • <App/> main component of our React app.
  • <List/> is our home-route component, showing a list of <Preview/> components.
  • <Preview/> shows the article title and thumbnail.
  • <Post/> is the article-route component, showing the article with its content. So our objective will be to transition from List to Post.

We’re going to use the react-transition-group package: add the dependency from your terminal:

yarn add react-transition-group

The <App/> component

Let’s start with the App component. The file ./src/App.js should be already created by create-react-app, so just replace its content with the below:

If you look at the render function, you’ll see that we have the List component and then the TransitionGroup component.

TransitionGroup is part of the react-transition-group package and will be responsible for managing the entering and exiting animations of its children.

As you can see, Post is a child of TransitionGroup and should only show when the name of the route is equal to ‘post’ . Therefore, when we will browse from the home route to the post route, the TransitionGroup should trigger the entering transition of the Post component.

navigateTo is the action from the redux-router package that allows to navigate from a route to another. We’re connecting it to the App component and passing it as a prop to both List and Post since both these components will need to trigger route changes.

Finally, from redux-router5 documentation:

routeNodeSelector is a selector designed to be used on a route node and works with connect higher-order component from react-redux.
Router5

In practice, routeNodeSelector passes the current route object as a prop, and triggers rendering route nodes only when it needs to (but we’ll come to that later).

<List/> and <Preview/>

Create the file ./src/List.js and paste the code:

In our case, List is a simple component, displaying a list of post previews. As you can see, clicking on a Preview instance should navigate to the post route, with id passed as a parameter.

Again, we’re using routeNodeSelector to pass the route object as a prop so that we can use the data fetched by our middleware in this.props.route.data.

Now let’s move to the Preview component inside ./src/Preview.js:

The only thing worth noting is the data-id html attribute that we’ll pass to the div. We’ll need it later when executing the transition (yep, we’ll need to manipulate the DOM directly: as mentioned before, Popmotion released react-pose which might change things).

The <Post/> component

Ok, so this is where everything happens, and we’ll take it slowly.

Create a ./src/Post.js file and paste the code below:

Since Post is going to be a children of a TransitionGroup, we need to embed its content inside a CSSTransition component. The reason why we’re using CSSTransition instead of Transition is because we’re going to use CSS classes to handle some parts of the transitions and because of this issue.

In this case, because we’ve set CSSTransition prop classNames="post", the following classes are going to be added and removed to our component parent div, and in this order: post-enter, post-enter-active, post-enter-done, post-exit, post-exit-active, post-exit-done.

Oh, and TransitionGroup actually passes props to its children. In particular, the in prop is a Boolean manages their in / out state, so we need to make sure we're passing all remaining props to the CSSTransition component if we want it to work properly.

The routeNodeSelector('post') makes sure the Post component gets access to the route object and also that only a post route or a post-nested route triggers a prop change.

Let me try to explain this better: here is what happens when triggering the exiting transition from the post route to the home route:

  1. [initial state] The route name is 'post' and the Post component uses this.props.route.data to display the post content.
  2. [user action] User clicks on the close button, that redirects to the home route.
  3. [router transition] The router transitions to the home route.
  4. [animation starts] The current route name is now 'home'. Since route.name is not equal to 'post' anymore, the Post component should not be displayed any longer. Therefore, TransitionGroup starts the exiting transition. However, if it wasn’t for routeNodeSelector('post'), the Post component would receive the home route as a prop, and you would get an error when trying to access this.props.route.data.post, because at this point, the route data actually contains a list of posts, and not an individual post.
  5. [animation completes] Once the exiting transition is completed, the Post component is finally removed from the DOM.

Connecting routeNodeSelector('selector') makes the connected component not care what happens to the router state as long as the route is not part of the 'selector' node. So the Post route prop won’t update when moving to the home route and the Post component will keep its data intact. I know this sounds a bit fuzzy but just try changing routeNodeSelector('post') to routeNodeSelector('') ('' is the selector for all routes) and see what happens.

Starting the App

Ok, we should be good. We just need a bunch of CSS and images to make all this work, so create the following files: ./src/index.scss, ./src/App.scss, ./src/List.scss and ./src/Post.scss.

Now paste these CSS classes in their respective files, and place these images inside the ./public/img directory.

Once you’ve saved the files, if you run yarn start, then all scss files should transpile into css (and this is why we’re not importing scss files in our JS in case you were wondering).

This is not a CSS tutorial (and again, I’m not good at Sass), but I just wanted to discuss the importance of the .page class.

.page {
position: absolute;
top: 0;
display: flex;
justify-content: center;
min-height: 100vh;
}

As you can see, the outer divs of both our List and Post components have the .page class. The reason why .page has an absolute positioning is because when transitioning, Post needs to be on top of List. If we had a relative positioning, Post would naturally follow the DOM flow and be positioned under List.

Testing the transition

You can now run the app with yarn start and let’s see what happens when clicking on an item from the List:

  • The URL changes
  • The Post appears right away

There’s one problem though. If you’ve scrolled through the List, you’ll see that the Post looks like it has been scrolled already. Also, if you scroll past the Post content, you should see the rest of the List overflowing. Don’t worry, we’ll fix this in Part 2.

This would actually be simple to fix if each page had height: 100vh and overflow: scroll CSS attributes, so the content would scroll inside the page. But remember, we want the native scroll so that the nav and tab bars of Safari mobile retract on scroll.

AFAIK, this behavior is only possible if the body element scrolls with the page, and doesn’t happen with overflowing divs.

And now let’s click on the close icon of the Post and go back to the List.

  • The URL changes
  • The Post components disappears after about 1 second.

This is because of the CSSTransition timeout prop, that we've set to 1000ms: Post is unmounted after 1 second. If you open the Inspector and select the .page-post element once the post route is active, you should see the classes .post-exit and .post-exit-active being added until the element is finally removed from the DOM.

If you look at the inspector after clicking on the close button, you’ll see the post-exit and post-exit-active classes being added.

You might notice you will get an error if you load the site from the post route (so http://localhost:3000/post/2 for example). This happens because the List component always shows no matter the route: when the initial route is the home route, then it gets the data it through the router middleware. However, when the initial route is the post route, then the component fails to render as no data is provided. Essentially, we want the List page to render when the home route is active, or when a previous route exists (in a real life scenario we would probably be a little more strict in the way we handle our components, but this works in this case).

So in the App.js file, replace the highlighted lines with the code below:

const { route, previousRoute, navigateTo } = this.props;/*...*/{(route.name === 'home' || previousRoute) && (
<List navigateTo={navigateTo} />
)}

That’s it for today. Here’s the link to the part-1 branch of the project should you need to take it from here.

Now let’s move on to the Part 2 of this tutorial to animate the transition.

--

--