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
AboutComponent.js
CampsiteInfoComponent.js
HomeComponent.js
MainComponent.js
DirectoryComponent.js
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
HomeComponent.js
DirectoryComponent.js
- 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:
- 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
ActionCreators.js
CampsiteInfoComponent.js
- 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:
- Redux.js – Three Principles of Redux
- React-Redux – Official React bindings for Redux
- Redux
- React-Redux
- Redux-Thunk
- Redux-Logger
- Json-Server
- React-Redux
- NPM – React DevTools
- React Native – Accessing the in-app developer menu
- Expo – Debugging
- Another option for debugging: React Native Debugger
- React Native – Introducing Hot Reloading
- React Native – Activity Indicator
- Redux.js – Using combineReducers
- Redux.js – Reducers
- Redux.js – Actions
- React-Redux – mapDispatchToProps
- W3Schools – JavaScript Array some() Method
- W3Schools – JavaScript Array concat() Method
- W3Schools – JavaScript Timing Events