Bootcamp Notes – Day 7 (Tues) – React Native: Week 3: Alerts, Animations, Gestures, and More!

Alerts, Animations, Gestures, and More!

In week 3 we will:

  • Reveal buttons in response to a swipe gesture.
  • Use the Alert API to show a dialog with options
  • Use the Animated library, a flexible and powerful way to create animations built into React Native
  • Also use a third party library called React Native Animatable to create basic animations quickly
  • Use the built-in PanResponder API to create custom reactions to gestures
  • Persist the Redux store through reloading of the application

Favorites

Notes about this step:

  1. Access campsites data from Redux state
  2. Use connect() function to connect to Redux store
  3. Use mapStateToProps function to pass campsites state to component as props
  4. No need for mapDispatchToProps object at this time
  5. Create a StackNavigator to use in the Drawer Navigator
  6. Set Stack Navigator header title using static navigationOptions
  7. Route to corresponding CampsiteInfo component using navigate function from the navigation prop
  8. Set up like Directory component, but with a filtered array of only favorited campsites instead of all campsites

 

  • Add a new file named FavoritesComponent.js in the components folder and update its contents as follows:
import React, { Component } from 'react';
import { FlatList, View, Text } from 'react-native';
import { ListItem } from 'react-native-elements';
import { connect } from 'react-redux';
import { Loading } from './LoadingComponent';
import { baseUrl } from '../shared/baseUrl';

const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        favorites: state.favorites
    };
};

class Favorites extends Component {

    static navigationOptions = {
        title: 'My Favorites'
    }

    render() {
        const { navigate } = this.props.navigation;
        const renderFavoriteItem = ({item}) => {
            return (
                <ListItem
                    title={item.name}
                    subtitle={item.description}
                    leftAvatar={{source: {uri: baseUrl + item.image}}}
                    onPress={() => navigate('CampsiteInfo', {campsiteId: item.id})}
                />
            );
        };

        if (this.props.campsites.isLoading) {
            return <Loading />;
        }
        if (this.props.campsites.errMess) {
            return (
                <View>
                    <Text>{this.props.campsites.errMess}</Text>
                </View>
            );
        }
        return (
            <FlatList
                data={this.props.campsites.campsites.filter(
                    campsite => this.props.favorites.includes(campsite.id)
                )}
                renderItem={renderFavoriteItem}
                keyExtractor={item => item.id.toString()}
            />
        );
    }
}

export default connect(mapStateToProps)(Favorites);

MainComponent.js

import React, { Component } from ‘react’;
import Home from ‘./HomeComponent’;
import Directory from ‘./DirectoryComponent’;
import CampsiteInfo from ‘./CampsiteInfoComponent’;
import About from ‘./AboutComponent’;
import Contact from ‘./ContactComponent’;
import Reservation from ‘./ReservationComponent’;
import Favorites from ‘./FavoritesComponent’;
import { View, Platform, StyleSheet, Text, ScrollView, Image } from ‘react-native’;
import { createStackNavigator } from ‘react-navigation-stack’;
import { createDrawerNavigator, DrawerItems } from ‘react-navigation-drawer’;
import { createAppContainer } from ‘react-navigation’;
import { Icon } from ‘react-native-elements’;
import SafeAreaView from ‘react-native-safe-area-view’;
import { connect } from ‘react-redux’;
import { fetchCampsites, fetchComments, fetchPromotions, fetchPartners } from ‘../redux/ActionCreators’;
const mapDispatchToProps = {
    fetchCampsites,
    fetchComments,
    fetchPromotions,
    fetchPartners
};
const DirectoryNavigator = createStackNavigator(
    {
        Directory: { 
            screen: Directory,
            navigationOptions: ({navigation}) => ({
                headerLeft: <Icon
                    name=’list’
                    type=’font-awesome’
                    iconStyle={styles.stackIcon}
                    onPress={() => navigation.toggleDrawer()}
                />
            })
        },
        CampsiteInfo: { screen: CampsiteInfo }
    },
    {
        initialRouteName: ‘Directory’,
        defaultNavigationOptions: {
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            }
        }
    }
);
const HomeNavigator = createStackNavigator(
    {
        Home: { screen: Home }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’home’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const AboutNavigator = createStackNavigator(
    {
        About: { screen: About }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’info-circle’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const ContactNavigator = createStackNavigator(
    {
        Contact: { screen: Contact }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’address-card’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const ReservationNavigator = createStackNavigator(
    {
        Reservation: { screen: Reservation }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’tree’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const FavoritesNavigator = createStackNavigator(
    {
        Favorites: { screen: Favorites }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’heart’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const CustomDrawerContentComponent = props => (
    <ScrollView>
        <SafeAreaView 
            style={styles.container}
            forceInset={{top: ‘always’, horizontal: ‘never’}}>
            <View style={styles.drawerHeader}>
                <View style={{flex: 1}}>
                    <Image source={require(‘./images/logo.png’)} style={styles.drawerImage} />
                </View>
                <View style={{flex: 2}}>
                    <Text style={styles.drawerHeaderText}>NuCamp</Text>
                </View>
            </View>
            <DrawerItems {…props} />
        </SafeAreaView>
    </ScrollView>
);
const MainNavigator = createDrawerNavigator(
    {
        Home: {
            screen: HomeNavigator,
            navigationOptions: {
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’home’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        Directory: {
            screen: DirectoryNavigator,
            navigationOptions: {
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’list’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        Reservation: {
            screen: ReservationNavigator,
            navigationOptions: {
                drawerLabel: ‘Reserve Campsite’,
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’tree’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        Favorites: {
            screen: FavoritesNavigator,
            navigationOptions: {
                drawerLabel: ‘My Favorites’,
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’heart’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        About: {
            screen: AboutNavigator,
            navigationOptions: {
                drawerLabel: ‘About Us’,
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’info-circle’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        Contact: {
            screen: ContactNavigator,
            navigationOptions: {
                drawerLabel: ‘Contact Us’,
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’address-card’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        }
    },
    {
        drawerBackgroundColor: ‘#CEC8FF’,
        contentComponent: CustomDrawerContentComponent
    }
);
const AppNavigator = createAppContainer(MainNavigator)
class Main extends Component {
    componentDidMount() {
        this.props.fetchCampsites();
        this.props.fetchComments();
        this.props.fetchPromotions();
        this.props.fetchPartners();
    }
    render() {
        return (
            <View style={{
                flex: 1,
                paddingTop: Platform.OS === ‘ios’ ? 0 : Expo.Constants.statusBarHeight
            }}>
                <AppNavigator />
            </View>
        );
    }
}
const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    drawerHeader: {
        backgroundColor: ‘#5637DD’,
        height: 140,
        alignItems: ‘center’,
        justifyContent: ‘center’,
        flex: 1,
        flexDirection: ‘row’
    },
    drawerHeaderText: {
        color: ‘#fff’,
        fontSize: 24,
        fontWeight: ‘bold’
    },
    drawerImage: {
        margin: 10,
        height: 60,
        width: 60
    },
    stackIcon: {
        marginLeft: 10,
        color: ‘#fff’,
        fontSize: 24
    }
});
export default connect(null, mapDispatchToProps)(Main);

Swipe Option Buttons and Alerts

Swipe option buttons are commonly seen added to items on lists in mobile applications. Mobile devices have limited screen size and mobile apps often hide options in this and similar ways. When you swipe on an item in the configured direction, previously hidden option buttons are revealed. There are various ways to create swipe option buttons like this. We will use react-native-swipe-list-view library!

Alert is a way of popping up information in a dialog, usually to confirm some operation with the user – similar to a modal, but simpler. React Native has a built-in API we can use to easily create an Alert dialog with a title, text, and buttons that works both on Android and iOS. In the example show here, there are two buttons: one to cancel, and one to confirm (the OK button). Tapping on either button will dismiss the Alert. We can further attach event handlers to either or both buttons. We will configure the confirm button to dispatch an action to the Redux store to delete this campsite from the list of favorites


Swipe and Delete Favorites

  • Install the react-native-swipe-list-view library to support a way to swipe to reveal option buttons:  expo install react-native-swipe-list-view

ActionTypes.js

export const CAMPSITES_LOADING = ‘CAMPSITES_LOADING’;
export const ADD_CAMPSITES = ‘ADD_CAMPSITES’;
export const CAMPSITES_FAILED = ‘CAMPSITES_FAILED’;
export const ADD_COMMENTS = ‘ADD_COMMENTS’;
export const COMMENTS_FAILED = ‘COMMENTS_FAILED’;
export const PROMOTIONS_LOADING = ‘PROMOTIONS_LOADING’;
export const ADD_PROMOTIONS = ‘ADD_PROMOTIONS’;
export const PROMOTIONS_FAILED = ‘PROMOTIONS_FAILED’;
export const PARTNERS_LOADING = ‘PARTNERS_LOADING’;
export const ADD_PARTNERS = ‘ADD_PARTNERS’;
export const PARTNERS_FAILED = ‘PARTNERS_FAILED’;
export const ADD_FAVORITE = ‘ADD_FAVORITE’;
export const ADD_COMMENT = ‘ADD_COMMENT’;
export const DELETE_FAVORITE = ‘DELETE_FAVORITE’; 

 

ActionCreators.js

import * as ActionTypes from ‘./ActionTypes’;
import { baseUrl } from ‘../shared/baseUrl’;
export const fetchComments = () => dispatch => {
    return fetch(baseUrl + ‘comments’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            })
        .then(response => response.json())
        .then(comments => dispatch(addComments(comments)))
        .catch(error => dispatch(commentsFailed(error.message)));
};
export const commentsFailed = errMess => ({
    type: ActionTypes.COMMENTS_FAILED,
    payload: errMess
});
export const addComments = comments => ({
    type: ActionTypes.ADD_COMMENTS,
    payload: comments
});
export const fetchCampsites = () => dispatch => {
    dispatch(campsitesLoading());
    return fetch(baseUrl + ‘campsites’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            })
        .then(response => response.json())
        .then(campsites => dispatch(addCampsites(campsites)))
        .catch(error => dispatch(campsitesFailed(error.message)));
};
export const campsitesLoading = () => ({
    type: ActionTypes.CAMPSITES_LOADING
});
export const campsitesFailed = errMess => ({
    type: ActionTypes.CAMPSITES_FAILED,
    payload: errMess
});
export const addCampsites = campsites => ({
    type: ActionTypes.ADD_CAMPSITES,
    payload: campsites
});
export const fetchPromotions = () => dispatch => {
    
    dispatch(promotionsLoading());
    return fetch(baseUrl + ‘promotions’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            })
        .then(response => response.json())
        .then(promotions => dispatch(addPromotions(promotions)))
        .catch(error => dispatch(promotionsFailed(error.message)));
};
export const promotionsLoading = () => ({
    type: ActionTypes.PROMOTIONS_LOADING
});
export const promotionsFailed = errMess => ({
    type: ActionTypes.PROMOTIONS_FAILED,
    payload: errMess
});
export const addPromotions = promotions => ({
    type: ActionTypes.ADD_PROMOTIONS,
    payload: promotions
});
export const fetchPartners = () => dispatch => {
    
    dispatch(partnersLoading());
    return fetch(baseUrl + ‘partners’)
        .then(response => {
                if (response.ok) {
                    return response;
                } else {
                    const error = new Error(`Error ${response.status}: ${response.statusText}`);
                    error.response = response;
                    throw error;
                }
            },
            error => {
                const errMess = new Error(error.message);
                throw errMess;
            })
        .then(response => response.json())
        .then(partners => dispatch(addPartners(partners)))
        .catch(error => dispatch(partnersFailed(error.message)));
};
export const partnersLoading = () => ({
    type: ActionTypes.PARTNERS_LOADING
});
export const partnersFailed = errMess => ({
    type: ActionTypes.PARTNERS_FAILED,
    payload: errMess
});
export const addPartners = partners => ({
    type: ActionTypes.ADD_PARTNERS,
    payload: partners
});
export const postFavorite = campsiteId => dispatch => {
    setTimeout(() => {
        dispatch(addFavorite(campsiteId));
    }, 2000);
};
export const addFavorite = campsiteId => ({
    type: ActionTypes.ADD_FAVORITE,
    payload: campsiteId
});
export const postComment = (campsiteId, rating, author, text) => dispatch => {
    const newComment = {
        campsiteId: campsiteId,
        rating: rating,
        author: author,
        text: text
    };
    newComment.date=new Date().toDateString();
    setTimeout(() => {
        dispatch(addComment(newComment));
    }, 2000);
}
export const addComment = comments => ({
    type: ActionTypes.ADD_COMMENT,
    payload: comments
});
export const deleteFavorite = campsiteId => ({
    type: ActionTypes.DELETE_FAVORITE,
    payload: campsiteId
}); 

favorites.js

import * as ActionTypes from ‘./ActionTypes’;
export const favorites = (state = [], action) => {
    switch (action.type) {
        case ActionTypes.ADD_FAVORITE:
            if (state.includes(action.payload)) {
                return state;
            }
            return state.concat(action.payload);
        case ActionTypes.DELETE_FAVORITE:
            return state.filter(favorite => favorite !== action.payload);
        default:
            return state;
    }
};

FavoritesComponent.js

import React, { Component } from ‘react’;
import { FlatList, View, Text, StyleSheet } from ‘react-native’;
import { ListItem } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
import { SwipeRow } from ‘react-native-swipe-list-view’;
import { TouchableOpacity } from ‘react-native-gesture-handler’;
import { deleteFavorite } from ‘../redux/ActionCreators’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        favorites: state.favorites
    };
};
const mapDispatchToProps = {
    deleteFavorite: campsiteId => deleteFavorite(campsiteId)
};
class Favorites extends Component {
    static navigationOptions = {
        title: ‘My Favorites’
    }
    render() {
        const { navigate } = this.props.navigation;
        const renderFavoriteItem = ({item}) => {
            return (
                <SwipeRow rightOpenValue={-100} style={styles.swipeRow}>
                    <View style={styles.deleteView}>
                        <TouchableOpacity
                        style={styles.deleteTouchable}
                        onPress={() => this.props.deleteFavorite(item.id)}
                        >
                        <Text style={styles.deleteText}>Delete</Text>
                        </TouchableOpacity>
                    </View>
                    <View>
                        <ListItem
                            title={item.name}
                            subtitle={item.description}
                            leftAvatar={{source: {uri: baseUrl + item.image}}}
                            onPress={() => navigate(‘CampsiteInfo’, {campsiteId: item.id})}
                        />
                    </View>
                </SwipeRow>
            );
        };
        if (this.props.campsites.isLoading) {
            return <Loading />;
        }
        if (this.props.campsites.errMess) {
            return (
                <View>
                    <Text>{this.props.campsites.errMess}</Text>
                </View>
            );
        }
        return (
            <FlatList
                data={this.props.campsites.campsites.filter(
                    campsite => this.props.favorites.includes(campsite.id)
                )}
                renderItem={renderFavoriteItem}
                keyExtractor={item => item.id.toString()}
            />
        );
    }
}
const styles = StyleSheet.create({
    deleteView: {
        flexDirection: ‘row’,
        justifyContent: ‘flex-end’,
        alignItems: ‘center’,
        flex: 1
    },
    deleteTouchable: {
        backgroundColor: ‘red’,
        height: ‘100%’,
        justifyContent: ‘center’
    },
    deleteText: {
        color: ‘white’,
        fontWeight: ‘700’,
        textAlign: ‘center’,
        fontSize: 16,
        width: 100
    }
});
export default connect(mapStateToProps, mapDispatchToProps)(Favorites);

Alerts

 

FavoritesComponent.js

import React, { Component } from ‘react’;
import { FlatList, View, Text, StyleSheet, Alert } from ‘react-native’;
import { ListItem } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
import { SwipeRow } from ‘react-native-swipe-list-view’;
import { TouchableOpacity } from ‘react-native-gesture-handler’;
import { deleteFavorite } from ‘../redux/ActionCreators’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        favorites: state.favorites
    };
};
const mapDispatchToProps = {
    deleteFavorite: campsiteId => deleteFavorite(campsiteId)
};
class Favorites extends Component {
    static navigationOptions = {
        title: ‘My Favorites’
    }
    render() {
        const { navigate } = this.props.navigation;
        const renderFavoriteItem = ({item}) => {
            return (
                <SwipeRow rightOpenValue={-100} style={styles.swipeRow}>
                    <View style={styles.deleteView}>
                        <TouchableOpacity
                        style={styles.deleteTouchable}
                        onPress={() =>
                            Alert.alert(
                                ‘Delete Favorite?’,
                                ‘Are you sure you wish to delete the favorite campsite ‘ +
                                    item.name +
                                    ‘?’,
                                [
                                    {
                                        text: ‘Cancel’,
                                        onPress: () => console.log(item.name + ‘Not Deleted’),
                                        style: ‘cancel’
                                    },
                                    {
                                        text: ‘OK’,
                                        onPress: () => this.props.deleteFavorite(item.id)
                                    },
                                ],
                                { cancelable: false }
                            )
                        }
                    >
                        <Text style={styles.deleteText}>Delete</Text>
                        </TouchableOpacity>
                    </View>
                    <View>
                        <ListItem
                            title={item.name}
                            subtitle={item.description}
                            leftAvatar={{source: {uri: baseUrl + item.image}}}
                            onPress={() => navigate(‘CampsiteInfo’, {campsiteId: item.id})}
                        />
                    </View>
                </SwipeRow>
            );
        };
        if (this.props.campsites.isLoading) {
            return <Loading />;
        }
        if (this.props.campsites.errMess) {
            return (
                <View>
                    <Text>{this.props.campsites.errMess}</Text>
                </View>
            );
        }
        return (
            <FlatList
                data={this.props.campsites.campsites.filter(
                    campsite => this.props.favorites.includes(campsite.id)
                )}
                renderItem={renderFavoriteItem}
                keyExtractor={item => item.id.toString()}
            />
        );
    }
}
const styles = StyleSheet.create({
    deleteView: {
        flexDirection: ‘row’,
        justifyContent: ‘flex-end’,
        alignItems: ‘center’,
        flex: 1
    },
    deleteTouchable: {
        backgroundColor: ‘red’,
        height: ‘100%’,
        justifyContent: ‘center’
    },
    deleteText: {
        color: ‘white’,
        fontWeight: ‘700’,
        textAlign: ‘center’,
        fontSize: 16,
        width: 100
    }
});
export default connect(mapStateToProps, mapDispatchToProps)(Favorites);

Animations

Subtle animations added to your application can go a long way in enhancing the user experience. React Native includes a powerful, declarative library called Animated. The Animated library is designed to make animations fluid, powerful, and painless to build and maintain. The most basic workflow for creating an animation is to create an Animated.Value, hook it up to one or more style attributes of an animated component, and then drive updates via animations using Animated.timing()

Components must be configured to be able to use the animated library. There is a wrapper function called create animated component that can be used to make a component animatable. You can use this on any component. But six of the most common animated components are already exported for you to use with this wrapper function as:

  • Animated.Image
  • Animated.ScrollView
  • Animated.Text
  • and so on!

 

Add an animation to the Home component, using React Native’s Animated API.

HomeComponent.js

import React, { Component } from ‘react’;
import { View, Text, Animated } from ‘react-native’;
import { Card } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
import Loading from ‘./LoadingComponent’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        promotions: state.promotions,
        partners: state.partners
    };
};
function RenderItem(props) {
    const {item} = props;
    if (props.isLoading) {
        return <Loading />;
    }
    if (props.errMess) {
        return (
            <View>
                <Text>{props.errMess}</Text>
            </View>
        );
    }
    if (item) {
        return (
            <Card
                featuredTitle={item.name}
                image={{uri: baseUrl + item.image}}>
                <Text style={{margin: 10}}>
                    {item.description}
                </Text>
            </Card>
        );
    }
    return <View />;
}
class Home extends Component {
    constructor(props) {
        super(props);
        this.state = {
            scaleValue: new Animated.Value(0)
        };
    }
    animate() {
        Animated.timing(
            this.state.scaleValue,
            {
                toValue: 1,
                duration: 1500,
                useNativeDriver: true
            }
        ).start();
    }
    componentDidMount() {
        this.animate();
    }
    static navigationOptions = {
        title: ‘Home’
    }
    static navigationOptions = {
        title: ‘Home’
    }
    render() {
        return (
            <Animated.ScrollView style={{transform: [{scale: this.state.scaleValue}]}}>
                <RenderItem
                    item={this.props.campsites.campsites.filter(campsite => campsite.featured)[0]}
                    isLoading={this.props.campsites.isLoading}
                    errMess={this.props.campsites.errMess}
                />
                <RenderItem
                    item={this.props.promotions.promotions.filter(promotion => promotion.featured)[0]}
                    isLoading={this.props.promotions.isLoading}
                    errMess={this.props.promotions.errMess}
                />
                <RenderItem
                    item={this.props.partners.partners.filter(partner => partner.featured)[0]}
                    isLoading={this.props.partners.isLoading}
                    errMess={this.props.partners.errMess}
                />
            </Animated.ScrollView>
        );
    }
}
export default connect(mapStateToProps)(Home);

 


The Animated API provides a fairly comprehensive way of doing complex animations, and comes included with the React Native core. But it can be quite cumbersome – a lot of setup for a simple animation. There are many third party libraries built on top of the Animated library to more easily set up basic animations. One is react native animatable which we will be using.


Animatable

  • First, install react-native-animatable in your nucampsite folder:  expo install react-native-animatable

 

AboutComponent.js 

import React, { Component } from ‘react’;
import { FlatList, Text, ScrollView } from ‘react-native’;
import { Card, ListItem } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
import Loading from ‘./LoadingComponent’;
import * as Animatable from ‘react-native-animatable’;
const mapStateToProps = state => {
    return {
        partners: state.partners
    };
};
class About extends Component {
    static navigationOptions = {
        title: ‘About Us’
    }
    render() {
        const renderPartner = ({item}) => {
            return (
                <ListItem
                    title={item.name}
                    subtitle={item.description}
                    leftAvatar={{source: {uri: baseUrl + item.image}}}
                />
            );
        };
        if (this.props.partners.isLoading) {
            return (
                <ScrollView>
                    <Mission />
                    <Card
                        title=’Community Partners’>
                        <Loading />
                    </Card>
                </ScrollView>
            );
        }
        if (this.props.partners.errMess) {
            return (
                <ScrollView>
                    <Animatable.View animation=’fadeInDown’ duration={2000} delay={1000}>
                        <Mission />
                        <Card
                            title=’Community Partners’>
                            <Text>{this.props.partners.errMess}</Text>
                        </Card>
                    </Animatable.View>
                </ScrollView>
            );
        }
            return (  
                <ScrollView>
                    <Animatable.View animation=’fadeInDown’ duration={2000} delay={1000}>
                        <Card title=’Mission’>
                            <Text wrapperStyle={{margin: 10}}>
                                We present a curated database of the best campsites in the vast woods and backcountry of the World Wide Web Wilderness. We increase access to adventure for the public while promoting safe and respectful use of resources. The expert wilderness trekkers on our staff personally verify each campsite to make sure that they are up to our standards. We also present a platform for campers to share reviews on campsites they have visited with each other. 
                            </Text> 
                        </Card>
                        <Card title=’Community Partners’>       
                        <FlatList 
                            data={this.props.partners.partners}
                            renderItem={renderPartner}
                            keyExtractor={item => item.id.toString()}
                        />
                        </Card>
                    </Animatable.View> 
                </ScrollView>
            );
        }
    }
           
export default connect(mapStateToProps)(About);

CampsiteInfoComponent.js

import React, { Component } from ‘react’;
import { Text, View, ScrollView, FlatList, Modal, Button, StyleSheet } from ‘react-native’;
import { Card, Icon, Rating, Input } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
import { postFavorite, postComment } from ‘../redux/ActionCreators’;
import * as Animatable from ‘react-native-animatable’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        comments: state.comments,
        favorites: state.favorites
    };
};
const mapDispatchToProps = {
    postFavorite: campsiteId => (postFavorite(campsiteId)),
    postComment: (campsiteId, rating, author, text) => postComment(campsiteId, rating, author, text)
};
function RenderCampsite(props) {
    const {campsite} = props;
    if (campsite) {
        return (
            <Animatable.View animation=’fadeInDown’ duration={2000} delay={1000}>
            <Card 
                featuredTitle={campsite.name}
                image={{uri: baseUrl + campsite.image}}>
            
                <Text style={{margin: 10}}>
                    {campsite.description}
                </Text>
                <View style={styles.cardRow}>
                    <Icon
                        name={props.favorite ? ‘heart’ : ‘heart-o’}
                        type=’font-awesome’
                        color=’#f50′
                        raised
                        reverse
                        onPress={() => props.favorite ? 
                            console.log(‘Already set as a favorite’) : props.markFavorite()}
                    />
                    <Icon
                        name=’pencil’
                        type=’font-awesome’
                        color=’#5637DD’
                        raised
                        reverse
                        onPress={() => props.onShowModal()} 
                            // console.log(‘Already set as a favorite’) : props.markFavorite()}
                    />
                </View>
            </Card>
            </Animatable.View>
        );
    }
    return <View />;
}
function RenderComments({comments}) {
    const renderCommentItem = ({item}) => {
        return (
            <View style={{margin: 10}}>
                <Text style={{fontSize: 14}}>{item.text}</Text>
                <Rating
                        startingValue={item.rating}
                        imageSize={10}
                        style={{paddingVertical: ‘5%’, alignItems: ‘flex-start’}}
                        readonly
                    />
                <Text style={{fontSize: 12}}>{`– ${item.author}, ${item.date}`}</Text>
            </View>
        );
    };
    return (
        <Animatable.View animation=’fadeInUp’ duration={2000} delay={1000}>
            <Card title=’Comments’>
                <FlatList
                    data={comments}
                    renderItem={renderCommentItem}
                    keyExtractor={item => item.id.toString()}
                />
            </Card>
        </Animatable.View>
    );
}
class CampsiteInfo extends Component {
    constructor(props) {
        super(props);
        this.state = {
            showModal: false,
            rating: 5,
            author: ” “,
            text: ” “
        };
    }
    toggleModal() {
        this.setState({showModal: !this.state.showModal});
    }
    handleComment(campsiteId) {
        this.props.postComment(campsiteId, this.state.rating, this.state.author, this.state.text);
        this.toggleModal();
    }
    resetForm() {
        this.setState({
            showModal: false,
            rating: 5,
            author: ” “,
            text: ” “
        });
    }
    markFavorite(campsiteId) {
        this.props.postFavorite(campsiteId);
    }
    static navigationOptions = {
        title: ‘Campsite Information’
    }
    render() {
        const campsiteId = this.props.navigation.getParam(‘campsiteId’);
        const campsite = this.props.campsites.campsites.filter(campsite => campsite.id === campsiteId)[0];
        const comments = this.props.comments.comments.filter(comment => comment.campsiteId === campsiteId);
        return (
            <ScrollView>
                <RenderCampsite campsite={campsite}
                    favorite={this.props.favorites.includes(campsiteId)}
                    markFavorite={() => this.markFavorite(campsiteId)}
                    onShowModal={() => this.toggleModal(campsiteId)}
                />
                <RenderComments comments={comments} />
                <Modal
                    animationType={‘slide’}
                    transparent={false}
                    visible={this.state.showModal}
                    onRequestClose={() => this.toggleModal()}
                >
                    <View style={styles.modal}>
                    <Rating
                        showRating
                        startingValue={this.state.rating}
                        imageSize={40}
                        onFinishRating={rating => this.setState({rating: rating})} 
                        style={{paddingVertical: 10}}
                    />
                    <Input
                        placeholder=’Author’
                        leftIcon={{ type: “font-awesome”, name: “user-o”}}
                        leftIconContainerStyle={{paddingRight: 10}}
                        onChangeText={value => this.setState({author: value})}
                        value={this.state.author}
                        />
                    <Input
                        placeholder=’Comment’
                        leftIcon={{ type: “font-awesome”, name: “comment-o”}}
                        leftIconContainerStyle={{paddingRight: 10}}
                        onChangeText={value => this.setState({text: value})}
                        value={this.state.text}
                        />
                        <View style={{margin: 10}}>
                        <Button
                            onPress={() => {
                                this.handleComment(campsiteId);
                                this.resetForm();
                            }}
                            color=’#5637DD’
                            title=’Submit’
                        />
                        </View>
                        <View style={{margin: 10}}>
                        <Button
                            onPress={() => {
                                this.toggleModal();
                                this.resetForm();
                            }}
                            color=’#808080′
                            title=’Cancel’
                        />
                        </View>
                    </View>
                </Modal>
            </ScrollView>
        );
    }
}
const styles = StyleSheet.create({
    cardRow: {
        alignItems: ‘center’,
        justifyContent: ‘center’,
        flex: 1,
        flexDirection: ‘row’,
        margin: 20
    },
    modal: { 
        justifyContent: ‘center’,
        margin: 20
    }
});
export default connect(mapStateToProps, mapDispatchToProps)(CampsiteInfo);

 

ContactComponent.js

import React, { Component } from ‘react’;
import { View, Text, ScrollView } from ‘react-native’;
import { Card } from ‘react-native-elements’;
import * as Animatable from ‘react-native-animatable’;
class Contact extends Component {
    static navigationOptions = {
        title: ‘Contact Us’
    }
    render() {
        return (
            <ScrollView>
                <Animatable.View animation=’fadeInDown’ duration={2000} delay={1000}>  
                    <Card title=’Contact Information’ wrapperStyle={{margin: 20}}>
                            <Text>1 Nucamp Way </Text>
                            <Text>Seattle, WA 98001</Text>
                            <Text style={{marginBottom: 10}} >U.S.A.</Text>
                        
                            <Text>Phone: 1-206-555-1234 </Text>
                            <Text>Email: campsites@nucamp.co</Text>
                    </Card>
               </Animatable.View>  
            </ScrollView>
        );
    }
}
export default Contact;

 

DirectoryComponent.js

import React, { Component } from ‘react’;
import { View, FlatList, Text } from ‘react-native’;
import { Tile } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
import Loading from ‘./LoadingComponent’;
import * as Animatable from ‘react-native-animatable’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
    };
};
class Directory extends Component {
    static navigationOptions = {
        title: ‘Directory’
    }
    render() {
        const { navigate } = this.props.navigation;
        const renderDirectoryItem = ({item}) => {
            return (
                <Animatable.View animation=’fadeInRightBig’ duration={2000}>
                    <Tile
                        title={item.name}
                        subtitle={item.description}
                        featured
                        onPress={() => navigate(‘CampsiteInfo’, { campsiteId: item.id })}
                        imageSrc={{uri: baseUrl + item.image}}
                    />
                </Animatable.View>
            );
        };
        if (this.props.campsites.isLoading) {
            return <Loading />;
        }
        if (this.props.campsites.errMess) {
            return (
                <View>
                    <Text>{this.props.campsites.errMess}</Text>
                </View>
            );
        }
        
        return (
            <FlatList
                data={this.props.campsites.campsites}
                renderItem={renderDirectoryItem}
                keyExtractor={item => item.id.toString()}
            />
        );
    }
}
export default connect(mapStateToProps)(Directory);

 

FavoritesComponent.js

import React, { Component } from ‘react’;
import { FlatList, View, Text, StyleSheet, Alert } from ‘react-native’;
import { ListItem } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
import { SwipeRow } from ‘react-native-swipe-list-view’;
import { TouchableOpacity } from ‘react-native-gesture-handler’;
import { deleteFavorite } from ‘../redux/ActionCreators’;
import * as Animatable from ‘react-native-animatable’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        favorites: state.favorites
    };
};
const mapDispatchToProps = {
    deleteFavorite: campsiteId => deleteFavorite(campsiteId)
};
class Favorites extends Component {
    static navigationOptions = {
        title: ‘My Favorites’
    }
    render() {
        const { navigate } = this.props.navigation;
        const renderFavoriteItem = ({item}) => {
            return (
                <SwipeRow rightOpenValue={-100} style={styles.swipeRow}>
                    <View style={styles.deleteView}>
                        <TouchableOpacity
                        style={styles.deleteTouchable}
                        onPress={() =>
                            Alert.alert(
                                ‘Delete Favorite?’,
                                ‘Are you sure you wish to delete the favorite campsite ‘ +
                                    item.name +
                                    ‘?’,
                                [
                                    {
                                        text: ‘Cancel’,
                                        onPress: () => console.log(item.name + ‘Not Deleted’),
                                        style: ‘cancel’
                                    },
                                    {
                                        text: ‘OK’,
                                        onPress: () => this.props.deleteFavorite(item.id)
                                    },
                                ],
                                { cancelable: false }
                            )
                        }
                    >
                        <Text style={styles.deleteText}>Delete</Text>
                        </TouchableOpacity>
                    </View>
                    <View>
                        <ListItem
                            title={item.name}
                            subtitle={item.description}
                            leftAvatar={{source: {uri: baseUrl + item.image}}}
                            onPress={() => navigate(‘CampsiteInfo’, {campsiteId: item.id})}
                        />
                    </View>
                </SwipeRow>
            );
        };
        if (this.props.campsites.isLoading) {
            return <Loading />;
        }
        if (this.props.campsites.errMess) {
            return (
                <View>
                    <Text>{this.props.campsites.errMess}</Text>
                </View>
            );
        }
        return (
            <Animatable.View animation=’fadeInRightBig’ duration={2000}>
                <FlatList
                    data={this.props.campsites.campsites.filter(
                        campsite => this.props.favorites.includes(campsite.id)
                    )}
                    renderItem={renderFavoriteItem}
                    keyExtractor={item => item.id.toString()}
                />
            </Animatable.View>
        );
    }
}
const styles = StyleSheet.create({
    deleteView: {
        flexDirection: ‘row’,
        justifyContent: ‘flex-end’,
        alignItems: ‘center’,
        flex: 1
    },
    deleteTouchable: {
        backgroundColor: ‘red’,
        height: ‘100%’,
        justifyContent: ‘center’
    },
    deleteText: {
        color: ‘white’,
        fontWeight: ‘700’,
        textAlign: ‘center’,
        fontSize: 16,
        width: 100
    }
});
export default connect(mapStateToProps, mapDispatchToProps)(Favorites);

 

 


Additional Resources: