Animated page transitions in reaction

For the last couple of weeks I have been working on an application using React. So far, everything is working fine, but now I want to add some transitions to it. These transitions are a bit more complicated than any examples I managed to find.

I have 2 pages, a review, and a detailed page, from which I would like to go.

I am using response-router to manage my routes:

<Route path='/' component={CoreLayout}> <Route path=':pageSlug' component={Overview} /> <Route path=':pageSlug/:detailSlug' component={DetailView} /> </Route> 

The general view is as follows: enter image description here

Detailview is as follows: enter image description here

The idea of ​​the transition is that you click on one of the Overview items. This item that was clicked moves to the position it should have on the detailView. The transition should be initiated by changing the route (I think), and should also be performed in the reverse order.

I already tried using ReactTransitionGroup in a layout that has a rendering method that looks like this:

 render () { return ( <div className='layout'> <ReactTransitionGroup> React.cloneElement(this.props.children, { key: this.props.location.pathname }) </ReactTransitionGroup> </div> ) } 

This will give the child component the ability to get special lifecycle bindings . But I would like to somehow access the child components during these intercepts and still continue to do React actions.

Can someone point me in the right direction for the next step? Or maybe give me an example that I might have missed? In previous projects, I used Ember along with liquid fire to get these kinds of transitions, maybe something similar for React?

I am using react / react-redux / react-router / react-router-redux .

+7
reactjs animation transitions
source share
1 answer

Edit: added working example

https://lab.award.is/react-shared-element-transition-example/

(Some problems in Safari for macOS for me)


The idea is for the elements to be animated, wrapped in a container that retains its position when mounted. I created a simple React component called SharedElement that does just that.

So, step by step for your example ( Overview view and Detailview ):

  • The Overview view is displayed. Each element (squares) inside the Survey is wrapped in a SharedElement with a unique identifier (for example, item-0, item-1, etc.). The SharedElement component saves the position for each item in the static variable Store (by the identifier you gave them).
  • Go to Detailview . A detailed view is enclosed in another SharedElement , which has the same identifier as the item you clicked on, for example, item-4.
  • Now this time, SharedElement sees that an item with the same identifier is already registered in its store. It will clone the new element, apply the position of the old elements to it (the one from Detailview), and animate the new position (I did this using GSAP). When the animation is complete, it overwrites the new position for the item in the store.

Using this method, it actually does not depend on the React Router (no special life cycle methods, but componentDidMount ), and it will work even when you first land on the first page of the review and go to the review page.

I will share my implementation with you, but remember that she has some known errors. For example. you have to deal with z-indeces and overwhelm yourself; and it does not process unregistered item positions from the store. I am sure that if someone can spend some time on this, you can make a great little plugin out of it.

Implementation:

index.js

 import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import Overview from './Overview' import DetailView from './DetailView' import "./index.css"; import { Router, Route, IndexRoute, hashHistory } from 'react-router' const routes = ( <Router history={hashHistory}> <Route path="/" component={App}> <IndexRoute component={Overview} /> <Route path="detail/:id" component={DetailView} /> </Route> </Router> ) ReactDOM.render( routes, document.getElementById('root') ); 

App.js

 import React, {Component} from "react" import "./App.css" export default class App extends Component { render() { return ( <div className="App"> {this.props.children} </div> ) } } 

Overview.js - Pay attention to the identifier in SharedElement

 import React, { Component } from 'react' import './Overview.css' import items from './items' // Simple array containing objects like {title: '...'} import { hashHistory } from 'react-router' import SharedElement from './SharedElement' export default class Overview extends Component { showDetail = (e, id) => { e.preventDefault() hashHistory.push(`/detail/${id}`) } render() { return ( <div className="Overview"> {items.map((item, index) => { return ( <div className="ItemOuter" key={`outer-${index}`}> <SharedElement id={`item-${index}`}> <a className="Item" key={`overview-item`} onClick={e => this.showDetail(e, index + 1)} > <div className="Item-image"> <img src={require(`./img/${index + 1}.jpg`)} alt=""/> </div> {item.title} </a> </SharedElement> </div> ) })} </div> ) } } 

DetailView.js - pay attention to the identifier in SharedElement

 import React, { Component } from 'react' import './DetailItem.css' import items from './items' import { hashHistory } from 'react-router' import SharedElement from './SharedElement' export default class DetailView extends Component { getItem = () => { return items[this.props.params.id - 1] } showHome = e => { e.preventDefault() hashHistory.push(`/`) } render() { const item = this.getItem() return ( <div className="DetailItemOuter"> <SharedElement id={`item-${this.props.params.id - 1}`}> <div className="DetailItem" onClick={this.showHome}> <div className="DetailItem-image"> <img src={require(`./img/${this.props.params.id}.jpg`)} alt=""/> </div> Full title: {item.title} </div> </SharedElement> </div> ) } } 

SharedElement.js

 import React, { Component, PropTypes, cloneElement } from 'react' import { findDOMNode } from 'react-dom' import TweenMax, { Power3 } from 'gsap' export default class SharedElement extends Component { static Store = {} element = null static props = { id: PropTypes.string.isRequired, children: PropTypes.element.isRequired, duration: PropTypes.number, delay: PropTypes.number, keepPosition: PropTypes.bool, } static defaultProps = { duration: 0.4, delay: 0, keepPosition: false, } storeNewPosition(rect) { SharedElement.Store[this.props.id] = rect } componentDidMount() { // Figure out the position of the new element const node = findDOMNode(this.element) const rect = node.getBoundingClientRect() const newPosition = { width: rect.width, height: rect.height, } if ( ! this.props.keepPosition) { newPosition.top = rect.top newPosition.left = rect.left } if (SharedElement.Store.hasOwnProperty(this.props.id)) { // Element was already mounted, animate const oldPosition = SharedElement.Store[this.props.id] TweenMax.fromTo(node, this.props.duration, oldPosition, { ...newPosition, ease: Power3.easeInOut, delay: this.props.delay, onComplete: () => this.storeNewPosition(newPosition) }) } else { setTimeout(() => { // Fix for 'rect' having wrong dimensions this.storeNewPosition(newPosition) }, 50) } } render() { return cloneElement(this.props.children, { ...this.props.children.props, ref: element => this.element = element, style: {...this.props.children.props.style || {}, position: 'absolute'}, }) } } 
+3
source share

All Articles