React Animations
When I talk about animations here, I don’t mean animations like a cartoon or something like that. I mean animations like fade effects, zooming and so on. Subtle animations in the user interface can improve the user experience. In this lesson and the next couple of exercises, we will take a look at how to add animations to our React Application.
REACT-TRANSITION-GROUP
There are many third-party libraries to bring animations into React. We will be using two of these. Once called: react-transition-group and the other one called react animation components.
react-transition-group used to be a part of the React core library, but now it is separately maintained.
react-transition-group provides a set of components that can recognize when other components are being mounted or unmounted and apply transition effects at that time.
There are only four components in react-transition-group
- <Transition>
- <CSSTransition>
- <SwitchTransition>
- <TransitionGroup>
We will be using <CSSTransition> and <TransitionGroup> to create a slide effect.
<CSSTransition> component lets us apply CSS classes at different stages of a component’s transition. So it is very open ended and flexible. We can use whatever CSS classes we want with it.
<TransitionGroup> wrapper component helps manage state of a group of components with transition effects applied.
REACT-ANIMATION-COMPONENTS is the other library we will be using. It actually makes use of the React transition group library itself and builds on it. This library provides drop-in GPU-accelerated animation components.
It has 6 components, 3 of which are animation components.
Animation Components:
- <Fade>
- <Transform>
- <FadeTransform> – (Let’s you do both a fade and a transform of the component at once with one component)
Wrapper Components:
- <Stagger> – (A wrapper component you can use on a group to stagger a delay on a set of animation components)
- <Random>
- <Loop>
Exercise: React Transitions
- We will use a third-party library called react-transition-group to animate transitions between views.
- Install the library in your project folder as follows, using yarn add: yarn add react-transition-group@4.4.1
App.css
Add this to your file:
.page-enter {
opacity: 0;
}
.page-enter-active {
opacity: 1;
transition: 300ms ease-out;
}
.page-exit {
opacity: 1;
}
.page-exit-active {
opacity: 0;
transition: 300ms ease-in;
}
.page-enter {
opacity: 0;
transform: translateX(-100%);
}
.page-enter-active {
opacity: 1;
transform: translateX(0%);
transition: 300ms ease-out;
}
MainComponent.js
import React, { Component } from ‘react’;
import Directory from ‘./DirectoryComponent’;
import CampsiteInfo from ‘./CampsiteInfoComponent’;
import Header from ‘./HeaderComponent’;
import Footer from ‘./FooterComponent’;
import Home from ‘./HomeComponent’;
import Contact from ‘./ContactComponent’;
import About from ‘./AboutComponent’;
import { Switch, Route, Redirect, withRouter } from ‘react-router-dom’;
import { connect } from ‘react-redux’;
import { actions } from ‘react-redux-form’;
import { postComment, fetchCampsites, fetchComments, fetchPromotions } from ‘../redux/ActionCreators’;
import { TransitionGroup, CSSTransition } from ‘react-transition-group’;
const mapStateToProps = state => {
return {
campsites: state.campsites,
comments: state.comments,
partners: state.partners,
promotions: state.promotions
};
};
const mapDispatchToProps = {
postComment: (campsiteId, rating, author, text) => (postComment(campsiteId, rating, author, text)),
fetchCampsites: () => (fetchCampsites()),
resetFeedbackForm: () => (actions.reset(‘feedbackForm’)),
fetchComments: () => (fetchComments()),
fetchPromotions: () => (fetchPromotions())
};
class Main extends Component {
componentDidMount() {
this.props.fetchCampsites();
this.props.fetchComments();
this.props.fetchPromotions();
}
render() {
const HomePage = () => {
return (
<Home
campsite={this.props.campsites.campsites.filter(campsite => campsite.featured)[0]}
campsitesLoading={this.props.campsites.isLoading}
campsitesErrMess={this.props.campsites.errMess}
promotion={this.props.promotions.promotions.filter(promotion => promotion.featured)[0]}
promotionLoading={this.props.promotions.isLoading}
promotionErrMess={this.props.promotions.errMess}
partner={this.props.partners.filter(partner => partner.featured)[0]}
/>
);
};
const CampsiteWithId = ({match}) => {
return (
<CampsiteInfo
campsite={this.props.campsites.campsites.filter(campsite => campsite.id === +match.params.campsiteId)[0]}
isLoading={this.props.campsites.isLoading}
errMess={this.props.campsites.errMess}
comments={this.props.comments.comments.filter(comment => comment.campsiteId === +match.params.campsiteId)}
commentsErrMess={this.props.comments.errMess}
postComment={this.props.postComment}
/>
);
};
return (
<div>
<Header />
<TransitionGroup>
<CSSTransition key={this.props.location.key} classNames=”page” timeout={300}>
<Switch>
<Route path=’/home’ component={HomePage} />
<Route exact path=’/directory’ render={() => <Directory campsites={this.props.campsites} />} />
<Route path=’/directory/:campsiteId’ component={CampsiteWithId} />
<Route exact path=’/contactus’ render={() => <Contact resetFeedbackForm={this.props.resetFeedbackForm} /> } />
<Route exact path=’/aboutus’ render={() => <About partners={this.props.partners} /> } />
<Redirect to=’/home’ />
</Switch>
</CSSTransition>
</TransitionGroup>
<Footer />
</div>
);
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Main));
Exercise: React Animation Components
You will learn to use react-animation-components to add more subtle animations to your React app.
Install react-animation-components into your React app as follows:
yarn add react-animation-components@3.0.0
yarn add prop-types@15.7.2
HomeComponents.js
import React from ‘react’;
import { Card, CardImg, CardText, CardBody, CardTitle } from ‘reactstrap’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
import { FadeTransform } from ‘react-animation-components’;
function RenderCard({item, isLoading, errMess}) {
if (isLoading) {
return <Loading />;
}
if (errMess) {
return <h4>{errMess}</h4>;
}
return (
<FadeTransform
in
transformProps={{
exitTransform: ‘scale(0.5) translateY(50%)’
}}>
<Card>
<CardImg src={baseUrl + item.image} alt={item.name} />
<CardBody>
<CardTitle>{item.name}</CardTitle>
<CardText>{item.description}</CardText>
</CardBody>
</Card>
</FadeTransform>
);
}
function Home(props) {
return (
<div className=”container”>
<div className=”row”>
<div className=”col-md m-1″>
<RenderCard
item={props.campsite}
isLoading={props.campsitesLoading}
errMess={props.campsitesErrMess}
/>
</div>
<div className=”col-md m-1″>
<RenderCard
item={props.promotion}
isLoading={props.promotionLoading}
errMess={props.promotionErrMess}
/>
</div>
<div className=”col-md m-1″>
<RenderCard item={props.partner} />
</div>
</div>
</div>
);
}
export default Home;
CampsiteInfoComponents.js
import React from ‘react’;
import { Card, CardImg, CardText, CardBody, Breadcrumb, BreadcrumbItem, Button, Modal, ModalHeader, ModalBody, Col, Row, Label } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
import { Control, LocalForm, Errors } from ‘react-redux-form’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
import { FadeTransform, Fade, Stagger } from ‘react-animation-components’;
const required = val => val && val.length;
const maxLength = len => val => !val || (val.length <= len);
const minLength = len => val => val && (val.length >= len);
const isNumber = val => !isNaN(+val);
const validEmail = val => /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(val);
class CommetForm extends React.Component {
constructor(props) {
super(props);
this.state = {
isModalOpen: false
};
this.toggleModal = this.toggleModal.bind(this);
}
handleSubmit(values) {
this.toggleModal();
this.props.postComment(this.props.campsiteId, values.rating, values.author, values.text);
}
toggleModal() {
this.setState({
isModalOpen: !this.state.isModalOpen
});
}
render() {
return (
<div>
<Button onClick={this.toggleModal} outline ><i className=”fa fa-pencil fa-lg” />Submit Comment</Button>{‘ ‘}
<Modal isOpen={this.state.isModalOpen} toggle={this.toggleModal}>
<ModalHeader toggle={this.toggleModal}>Submit Comment</ModalHeader>
<ModalBody>
<LocalForm onSubmit={values => this.handleSubmit(values)}>
<div className=”form-group”>
<Label htmlFor=”rating”>Rating</Label>
<Control.select model=”.rating” id=”rating” name=”rating”
className=”form-control”>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
</Control.select>
</div>
<div className=”form-group”>
<Label htmlFor=”author” >Your Name</Label>
<Control.text model=”.author” id=”author” name=”author”
placeholder=”Your Name”
className=”form-control”
validators={{
required,
minLength: minLength(2),
maxLength: maxLength(15)
}}
/>
<Errors
className=”text-danger”
model=”.firstName”
show=”touched”
component=”div”
messages={{
required: ‘Required’,
minLength: ‘Must be at least 2 characters’,
maxLength: ‘Must be 15 characters or less’
}}
/>
</div>
<div className=”form-group”>
<Label htmlFor=”text” >Comment</Label>
<Control.textarea model=”.text” id=”text” name=”text”
rows=”12″
className=”form-control”
/>
</div>
<Button type=”submit” color=”primary”>
Submit
</Button>
</LocalForm>
</ModalBody>
</Modal>
</div>
);
}
}
function RenderCampsite({campsite}) {
return (
<div className=”col-md-5 m-1″>
<FadeTransform
in
transformProps={{
exitTransform: ‘scale(0.5) translateY(-50%)’
}}>
<Card>
<CardImg top src={baseUrl + campsite.image} alt={campsite.name} />
<CardBody>
<CardText>{campsite.description}</CardText>
</CardBody>
</Card>
</FadeTransform>
</div>
);
}
function RenderComments({comments, postComment, campsiteId}) {
if (comments) {
return (
<div className=”col-md-5 m-1″>
<h4>Comments</h4>
<Stagger in>
{
comments.map(comment => {
return (
<Fade in key={comment.id}>
<div>
<p>
{comment.text}<br />
— {comment.author}, {new Intl.DateTimeFormat(‘en-US’, { year: ‘numeric’, month: ‘short’, day: ‘2-digit’}).format(new Date(Date.parse(comment.date)))}
</p>
</div>
</Fade>
);
})
}
</Stagger>
<CommetForm campsiteId={campsiteId} postComment={postComment} />
</div>
);
}
return <div />;
}
function CampsiteInfo(props) {
if (props.isLoading) {
return (
<div className=”container”>
<div className=”row”>
<Loading />
</div>
</div>
);
}
if (props.errMess) {
return (
<div className=”container”>
<div className=”row”>
<div className=”col”>
<h4>{props.errMess}</h4>
</div>
</div>
</div>
);
}
if (props.campsite) {
return (
<div className=”container”>
<div className=”row”>
<div className=”col”>
<Breadcrumb>
<BreadcrumbItem><Link to=”/directory”>Directory</Link></BreadcrumbItem>
<BreadcrumbItem active>{props.campsite.name}</BreadcrumbItem>
</Breadcrumb>
<h2>{props.campsite.name}</h2>
<hr />
</div>
</div>
<div className=”row”>
<RenderCampsite campsite={props.campsite} />
<RenderComments
comments={props.comments}
postComment={props.postComment}
campsiteId={props.campsite.id}
/>
</div>
</div>
);
}
return <div />;
}
export default CampsiteInfo;
Additional Resources: