Bootcamp Notes – Day 5 (Tues) – React Native: Week 2: Introduction to Redux for React Native

Introduction to Redux for React Native

 

 

Redux Refresher

  • Remember, Redux is a predictable state container for JavaScript apps.
  • Originally created for use with React, but can be used with any JavaScript application.
  • Influenced by the Flux architectural pattern, especially one of Flux’s primary features: one-way data flow

THREE PRINCIPLES OF REDUX

  • Single source of truth
  • State is read-only
  • Changes are made with pure function

Single source of truth: The Redux store contains a single state object tree that hold all the application state information for the client side. React components are able to subscribe to this store, then whenever the state gets changed, the components re-render themselves to reflect that change.

State is read-only: The state object is never mutated. The state object can be replaced by a new state object, but it is never modified. Think about it as a flipbook, where every page is different.

Changes are made with pure functions: The only way that changes happen to the state is through actions, which are plain objects. These action objects are dispatched to pure functions that are called reducers.

A pure function always gives the same output when given the same inputs. A reducer function takes the old state and an action as its input, then returns a new state object as its output, which then replaces the old state in the store.

Redux Thunk: Allows you to write action creators that return a function instead of an action object, used to inject additional operations such as delaying an action from being dispatched to the reducer.

Asynchronous calls using the Fetch API to send a request to a server from an action creator, then wait for a response before continuing.


 Setting Up Redux

Json-server

  • Recall the json-server you installed in the React course.
  • Open a bash terminal to your json-server folder that you created in the React course at NucampFolder/json-server.
  • If you have updated your Node version, your global installation of json-server from the React course may no longer be present. To check, enter into your terminal from any location: json-server -v
  • If this gives you a version number, your json-server is still installed. If not, you can install it again globally. Use ctrl-c to stop any Expo project you have running before using the next command. Once you are sure no Expo project is running, enter:  yarn global add json-server
  • Start up json-server:  json-server -H 0.0.0.0 –watch db.json -p 3001 -d 2000
  • Compare your json-server output to the below images to make sure that you started your json-server in the correct location.
  • If  you started json-server in the correct location, your terminal will show the below output, with the campsites, comments, partners, promotions, and feedback resources.

  • If you started json-server in an incorrect location, your terminal will create a default database file and will show posts, comments, and profile resources from that default file, as you see below. If your json-server output looks like this, you are not in the json-server folder with the Nucamp-provided db.json file and you need to change directories to that folder.

  • Be careful – while you will start json-server from inside the json-server folder, all other commands (such as expo start and expo install) should be run from inside your project (4-React-Native/nucampsite) folder.

Install Redux and more

  • Using a different instance of your bash terminal, make sure you are in your React Native folder’s nucampsite project folder. Install Redux and related packages:
expo install redux@4.0.5 react-redux@7.2.0 redux-thunk@2.3.0 redux-logger@3.0.6

Set up baseUrl.js

  • Find out the IP address of your computer. In macOS, go to System Preferences->Network to find the IP address of your network adapter. In Windows, you can use the instructions available at https://www.digitalcitizen.life/find-ip-address-windows to find your machine’s IP address. Note: The ipconfig command shown in the instructions can be used from Git Bash. Look for the IPv4 address, not the IPv6.
  • Next, create a file named baseUrl.js in the shared folder and add the following to it:  export const baseUrl = ‘http://<Your Computer’s IP address>:3001/’;
  • For example, if your IP address is 192.168.1.121, then the above line should be as follows:  export const baseUrl = ‘http://192.168.1.121:3001/’;
  • Don’t miss that ending slash after 3001! It’s a small but very important detail.

Next Steps:

  • Then, create a folder named redux in your project
  • First, add a new file named ActionTypes.js in the redux folder, and add the following to it:

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';
  • Then, add a file named configureStore.js in the redux folder and add the following to it:

configureStore.js

import {createStore, combineReducers, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { campsites } from './campsites';
import { comments } from './comments';
import { promotions } from './promotions';
import { partners } from './partners';

export const ConfigureStore = () => {
    const store = createStore(
        combineReducers({
            campsites,
            comments,
            partners,
            promotions
        }),
        applyMiddleware(thunk, logger)
    );

    return store;
}
  • Next, we add the reducers. Add a file named campsites.js in the redux folder and update it as follows:

campsites.js

import * as ActionTypes from './ActionTypes';

export const campsites = (state = { isLoading: true,
                                     errMess: null,
                                     campsites: []}, action) => {
    switch (action.type) {
        case ActionTypes.ADD_CAMPSITES:
            return {...state, isLoading: false, errMess: null, campsites: action.payload};

        case ActionTypes.CAMPSITES_LOADING:
            return {...state, isLoading: true, errMess: null, campsites: []}

        case ActionTypes.CAMPSITES_FAILED:
            return {...state, isLoading: false, errMess: action.payload};

        default:
            return state;
      }
};
  • Then, add a file named comments.js in the redux folder and update it as follows:. . .

 

comments.js

import * as ActionTypes from './ActionTypes';

export const comments = (state = { errMess: null, comments: []}, action) => {
    switch (action.type) {
        case ActionTypes.ADD_COMMENTS:
            return {...state, errMess: null, comments: action.payload};

        case ActionTypes.COMMENTS_FAILED:
            return {...state, errMess: action.payload};

        default:
            return state;
    }
};
  • Next, add a file named partners.js in the redux folder and update it as follows:

 partners.js

import * as ActionTypes from './ActionTypes';

export const partners = (state = { isLoading: true,
                                    errMess: null,
                                    partners: []}, action) => {
    switch (action.type) {
        case ActionTypes.ADD_PARTNERS:
            return {...state, isLoading: false, errMess: null, partners: action.payload};

        case ActionTypes.PARTNERS_LOADING:
            return {...state, isLoading: true, errMess: null, partners: []}

        case ActionTypes.PARTNERS_FAILED:
            return {...state, isLoading: false, errMess: action.payload};

        default:
            return state;
    }
};

Then, add a file named promotions.js in the redux folder and add the following to it:

promotions.js 

import * as ActionTypes from './ActionTypes';

export const promotions = (state = { isLoading: true,
                                        errMess: null,
                                        promotions: []}, action) => {
    switch (action.type) {
        case ActionTypes.ADD_PROMOTIONS:
            return {...state, isLoading: false, errMess: null, promotions: action.payload};

        case ActionTypes.PROMOTIONS_LOADING:
            return {...state, isLoading: true, errMess: null, promotions: []}

        case ActionTypes.PROMOTIONS_FAILED:
            return {...state, isLoading: false, errMess: action.payload};

        default:
            return state;
      }
}; Then, add a file named ActionCreators.js in the redux folder and add the following to it:

 

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
});

Using Redux in React Native

  • Set up your React Native app to connect the components to Redux.
  • Obtain state information from Redux.
  • Dispatch actions to the Redux store to update the state.
  • Make sure to start your json-server before all following exercises, from the json-server folder that holds the db.json file.    json-server -H 0.0.0.0 –watch db.json -p 3001 -d 2000

App.js

import React from ‘react’;
import Main from ‘./components/MainComponent’;
import { Provider } from ‘react-redux’;
import { ConfigureStore } from ‘./redux/configureStore’;
const store = ConfigureStore();
export default function App() {
    return (
        <Provider store={store}>
          <Main />
        </Provider>
    );
}

 

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’;
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}}}
                />
            );
        }
            return (  
                <ScrollView>
                    <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> 
                </ScrollView>
            );
        }
    }
           
export default connect(mapStateToProps)(About);

 

CampsiteInfoComponent.js

import React, { Component } from ‘react’;
import { Text, View, ScrollView, FlatList } from ‘react-native’;
import { Card, Icon } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        comments: state.comments
    };
};
function RenderCampsite(props) {
    const {campsite} = props;
    if (campsite) {
        return (
            <Card 
                featuredTitle={campsite.name}
                image={{uri: baseUrl + campsite.image}}>
            
                <Text style={{margin: 10}}>
                    {campsite.description}
                </Text>
                <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()}
                />
            </Card>
        );
    }
    return <View />;
}
function RenderComments({comments}) {
    const renderCommentItem = ({item}) => {
        return (
            <View style={{margin: 10}}>
                <Text style={{fontSize: 14}}>{item.text}</Text>
                <Text style={{fontSize: 12}}>{item.rating} Stars</Text>
                <Text style={{fontSize: 12}}>{`– ${item.author}, ${item.date}`}</Text>
            </View>
        );
    };
    return (
        <Card title=’Comments’>
            <FlatList
                data={comments}
                renderItem={renderCommentItem}
                keyExtractor={item => item.id.toString()}
            />
        </Card>
    );
}
class CampsiteInfo extends Component {
    constructor(props) {
        super(props);
        this.state = { 
            favorite: false
        };
    }
    markFavorite() {
        this.setState({favorite: true});
    }
    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.state.favorite}
                    markFavorite={() => this.markFavorite()}
                />
                <RenderComments comments={comments} />
            </ScrollView>
        );
    }
}
export default connect(mapStateToProps)(CampsiteInfo);

HomeComponent.js

import React, { Component } from ‘react’;
import { View, Text, ScrollView } from ‘react-native’;
import { Card } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
const mapStateToProps = state => {
    return {
        campsites: state.campsites,
        promotions: state.promotions,
        partners: state.partners
    };
};
function RenderItem({item}) {
    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 {
    static navigationOptions = {
        title: ‘Home’
    }
    render() {
        return (
            <ScrollView>
                <RenderItem 
                    item={this.props.campsites.campsites.filter(campsite => campsite.featured)[0]}
                />
                <RenderItem 
                    item={this.props.promotions.promotions.filter(promotion => promotion.featured)[0]}
                />
                <RenderItem 
                    item={this.props.partners.partners.filter(partner => partner.featured)[0]}
                />
            </ScrollView>
        );
    }
}
export default connect(mapStateToProps)(Home);

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 { 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 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}
                    />
                )
            }
        },
        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);

 

DirectoryComponent.js

import React, { Component } from ‘react’;
import { FlatList } from ‘react-native’;
import { Tile } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
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 (
                <Tile
                    title={item.name}
                    subtitle={item.description}
                    featured
                    onPress={() => navigate(‘CampsiteInfo’, { campsiteId: item.id })}
                    imageSrc={{uri: baseUrl + item.image}}
                />
            );
        };
        return (
            <FlatList
                data={this.props.campsites.campsites}
                renderItem={renderDirectoryItem}
                keyExtractor={item => item.id.toString()}
            />
        );
    }
}
export default connect(mapStateToProps)(Directory);

 

Test your app. Make sure json-server is running. If your pages are loading blank, and you’re seeing error logs in the bash terminal where you used expo start, React Native app may be having trouble fetching your files from json-server. Make sure that your json-server is running, and that it was started in the correct folder. Your terminal with json-server running should look like this after you start json-server:


Debugging

  • Learn to access the Expo Developer Menu and look at how to use the remote JS debugging and inspector options
  • Install and use the standalone React-Devtools

Logging to console & the Expo Developer Menu

  • Open your React Native application via Expo on your emulator or mobile device.
  • Notice that redux-logger is logging messages to the console in your bash terminal where you gave the command expo start.
  • Also recall that you can log messages to the bash terminal as well, as you did when you added a console.log for when an already favorited campsite is favorited again.
  • However, notice that it is difficult to read all the logs to console in the bash terminal. There’s another way:
  • Expo provides an in-app Developer Menu.
  • In your Android emulator, type Ctrl-M (Windows/Linux) or Command-M (macOS) to open the Developer Menu.
  • If you are testing using an actual mobile device instead of an emulator, then shake your device slightly to open the Developer Menu.
  • If for some reason, Ctrl-M or Command-M are not working for you in the emulator, open a new bash terminal session and enter this command (from any directory) to open the Developer Menu:  adb shell input keyevent 82
  • From the menu, select Debug Remote JS – this will open a debugger tab in your default web browser.
  • From there, you can open the browser developer tools and go to the Console tab to see the redux-logger (and any other console.log) messages in a more convenient way.
  •  Debug Remote JS runs your JS code in a web worker thread in Chrome. Running JS remotely will make your app run slowly, so turn off remote debugging after you are done.

Standalone React-Devtools

  • IMPORTANT!: If you have any Expo project or json-server running, shut it down using ctrl-c. Otherwise, the following installation will fail due to globally installed Yarn packages locking files when in use, and Yarn global installations may become corrupted. Check all open terminals and make sure you do not have any Expo project or json-server running.
  • Globally install the standalone react-devtools as follows (from your bash terminal in any directory): yarn global add react-devtools@4.8.2
  • Now you can restart your expo project, from the 4-React-Native/nucampsites folder, using expo start. You can restart your json-server as well (from the json-server folder as usual). Then run the app in your emulator or device.
  • Start the standalone React Devtools by typing react-devtools into your terminal (don’t forget the hyphen).
  • If you are using a real mobile device and not the Android emulator, open a new bash terminal and enter this command:  adb reverse tcp:8097 tcp:8097
  • React Devtools should connect to your React Native application in a few seconds. If it does not, reload your application in your emulator or device. (Tip: You can use the keyboard shortcut ‘rr‘ to reload your application.) Or, open the Expo Developer Menu (with Ctrl/Command-M) and select Debug Remote JS.
  • Once React Devtools is connected, you will be able to look at the components along with their state, props, and styles in React Devtools.
  • From here, you can open the Expo Developer Menu and choose the Show Element Inspector option, which allows you to select a component from the UI of your emulator or mobile device in order to see it in React Devtools.
  • These are a few of the options you have when you want to debug/look more closely at your app. Keep them in mind in case you run into issues in the future where it would help to be able to take a closer look at your app in action.


  • Create a new Loading component, using the ActivityIndicator component from React Native.
  • Update the other components so that they show the Loading component while data is being fetched from the server, or an error if an error is returned from the fetch call to the server.
  • Add a new file named LoadingComponent.js to the components folder and update it as follows:

LoadingComponent.js

    import React from 'react';
    import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
    
    function Loading() {
        return (
            <View style={styles.loadingView}>
                <ActivityIndicator size='large' color='#5637DD' />
                <Text style={styles.loadingText}>Loading . . .</Text>
            </View>
        );
    }
    
    const styles = StyleSheet.create(
        {
            loadingView: {
                alignItems: 'center',
                justifyContent: 'center',
                flex: 1
            },
            loadingText: {
                color: '#5637DD',
                fontSize: 14,
                fontWeight: 'bold'
            }
        }
    );
    
    export default Loading;

    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’;
    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>
                        <Mission />
                        <Card
                            title=’Community Partners’>
                            <Text>{this.props.partners.errMess}</Text>
                        </Card>
                    </ScrollView>
                );
            }
                return (  
                    <ScrollView>
                        <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> 
                    </ScrollView>
                );
            }
        }
               
    export default connect(mapStateToProps)(About);

     

    HomeComponent.js

    import React, { Component } from ‘react’;
    import { View, Text, ScrollView } 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 {
        static navigationOptions = {
            title: ‘Home’
        }
        render() {
            return (
                <ScrollView>
                    <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}
                    />
                </ScrollView>
            );
        }
    }
    export default connect(mapStateToProps)(Home);

     

    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’;
    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 (
                    <Tile
                        title={item.name}
                        subtitle={item.description}
                        featured
                        onPress={() => navigate(‘CampsiteInfo’, { campsiteId: item.id })}
                        imageSrc={{uri: baseUrl + item.image}}
                    />
                );
            };
            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);
    • Test your application. Don’t forget to make sure you have json-server running from the correct folder. Make sure that when you reload your app, you see the ActivityIndicator spinner (there should be three of them) on the Home page. Then if you stop your json-server and wait, you should eventually see a “Network request failed” error message.

    Redux Adding Favorites

    Now that we have the Redux store set up, we will transition to using Redux to manage the favorites information. Up to now, the CampsiteInfo component has been responsible for keeping track of whether a campsite has been favorited or not, within it’s local state. That approach caused the favorites data to be cleared whenever the CampsiteInfo component was re-rendered. We will update the code to instead keep an array that contains the IDs of the favorite campsites in the Redux store.

    • Set up a new array property in the Redux store to track favorites.
    • Set up actions and a reducer to handle adding a new favorite to the store.
    • Update the CampsiteInfo component to use Redux to handle tracking favorites.

    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’;
    • Then, create a new file named favorites.js in the redux folder with its contents as follows:

     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);
    
            default:
                return state;
        }
    };

    configureStore.js

    import {createStore, combineReducers, applyMiddleware} from ‘redux’;
    import thunk from ‘redux-thunk’;
    import logger from ‘redux-logger’;
    import { campsites } from ‘./campsites’;
    import { comments } from ‘./comments’;
    import { promotions } from ‘./promotions’;
    import { partners } from ‘./partners’;
    import { favorites } from ‘./favorites’;
    export const ConfigureStore = () => {
        const store = createStore(
            combineReducers({
                campsites,
                comments,
                partners,
                promotions,
                favorites
            }),
            applyMiddleware(thunk, logger)
        );
        return store;
    }

    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
    });

     

    CampsiteInfoComponent.js

    import React, { Component } from ‘react’;
    import { Text, View, ScrollView, FlatList } from ‘react-native’;
    import { Card, Icon } from ‘react-native-elements’;
    import { connect } from ‘react-redux’;
    import { baseUrl } from ‘../shared/baseUrl’;
    import { postFavorite } from ‘../redux/ActionCreators’;
    const mapStateToProps = state => {
        return {
            campsites: state.campsites,
            comments: state.comments,
            favorites: state.favorites
        };
    };
    const mapDispatchToProps = {
        postFavorite: campsiteId => (postFavorite(campsiteId))
    };
    function RenderCampsite(props) {
        const {campsite} = props;
        if (campsite) {
            return (
                <Card 
                    featuredTitle={campsite.name}
                    image={{uri: baseUrl + campsite.image}}>
                
                    <Text style={{margin: 10}}>
                        {campsite.description}
                    </Text>
                    <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()}
                    />
                </Card>
            );
        }
        return <View />;
    }
    function RenderComments({comments}) {
        const renderCommentItem = ({item}) => {
            return (
                <View style={{margin: 10}}>
                    <Text style={{fontSize: 14}}>{item.text}</Text>
                    <Text style={{fontSize: 12}}>{item.rating} Stars</Text>
                    <Text style={{fontSize: 12}}>{`– ${item.author}, ${item.date}`}</Text>
                </View>
            );
        };
        return (
            <Card title=’Comments’>
                <FlatList
                    data={comments}
                    renderItem={renderCommentItem}
                    keyExtractor={item => item.id.toString()}
                />
            </Card>
        );
    }
    class CampsiteInfo extends Component {
        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)}
                    />
                    <RenderComments comments={comments} />
                </ScrollView>
            );
        }
    }
    export default connect(mapStateToProps, mapDispatchToProps)(CampsiteInfo);
    • Test the changes.
      • Don’t forget to make sure you have json-server running from the correct folder.
      • Make sure that when you mark a new favorite, there is a 2 second delay.
      • Then, if you leave the CampsiteInfo screen for that campsite and return to it, it should have retained its favorite status.
      • Test with at least two different campsites.

     


    Additional Comments:

    You May Also Like