Tim Garcia
- Computer Science undergrad @ UC Riverside, masters @ Georgia Tech
- 5 years @ Amazon working on different kindle products (kindle readers, kindle fire table, kindle cloud reader, etc)
- 3 Years teaching at Web Development Bootcamps
- Co-founder and Lead Instructor at Rithm School.
Contact: Please get in touch!
- Why React Native?
- Setup react-native toolchain, and walk through the files in a new application
- Create scene with basic react native components (Text, View, Button, TouchableOpacity)
- Get familiar with styling in react native (flex, flexDirection, justifyContent, alignItems)
- Use props, state to make app welcome page
- Debug your react-native app
- Practice ES2015 syntax for classes, import, arrow functions, const, let, string templates, spread operator
- Use lifecycle events like
componentDidMount
,componentWillUnmount
, andshouldComponentUpdate
- Understand the state of navigation in react-native and implement navigation with react-nativation
React native is a library for creating native applications. It allows developers to write JS code that produces iOS and Android binaries. It was first announced to the world in March 2015 and has been changing and growing in popularity at high speeds
Advantages
- Shared code between iOS and Android
- More JS developers available than native developers
- Faster iteration when developing (hot reloading, easier to reason about state)
- Can prototype quickly
Disadvantages
- Not as performant as native
- Fast paced community. Lots of change
The react native docs have a great break down of what to do on each platform. Here are the highlights:
For Everyone:
- Install nodejs
- Run
npm install -g react-native-cli
to install the command line tool for react native - Run
brew install node && brew install watchman
(On mac)
iOS:
- Make sure you have xcode installed. If you do not, start downloading now!!
Android:
- Download and install android studio. Follow the direction in the facebook docs.
In your terminal, run:
react-native init SongGuessingGame
Note that SongGuessingGame
is the name of our project and can be anything.
The cli will create a folder and install the files you need inside. Here are some important ones:
- package.json: Lists our javascript dependencies. This is what gets edited when you do
npm install --save <module_name>
- index.ios.js: The entry point to our ios app. We can put ios specific setup here.
- index.android.js: The entry point to our android app. Android specific setup can go here
- .babelrc: The config file for babel (a transpiling library)
- .flowconfig: Flow is a static type checker than you can use in your app. Find out more from facebook
- node_modules: A directory that contains all of our JS dependencies.
- ios: A directory that contains build settings for iOS. The xcode project will be found here.
- android: A directory that contains build settings for android. The gradle build scrips will be in here.
React native does not use the DOM, so no html. Instead we must use native components. We'll start with:
- Text: For putting text on the screen
- View: A generic component that holds other components
- Button: A non customizable button that attempts to look and feel like a native on iOS and android.
- TouchableOpacity: A touchable wrapper that is typically used to make custom buttons
Take a look at the code that was generated in index.ios.js (or similarly in index.android.js). Try to figure out what it does:
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
export default class SongGuessingGame extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit index.ios.js
</Text>
<Text style={styles.instructions}>
Press Cmd+R to reload,{'\n'}
Cmd+D or shake for dev menu
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
AppRegistry.registerComponent('SongGuessingGame', () => SongGuessingGame);
For our first component, create a directory in the root of your project called src
. Inside of src
, create a file called Welcome.js
.
import React, { Component } from 'react';
import {
View,
Text,
StyleSheet,
} from 'react-native'
export default class Welcome extends Component {
render() {
return (
<View style={styles.container}>
<Text>Hello React-Native</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
}
});
Now change your index.ios.js and index.andriod.js to use welcome:
import React, { Component } from 'react';
import { AppRegistry } from 'react-native';
import Welcome from './src/Welcome'
export default class SongGuessingGame extends Component {
render() {
return (
<Welcome />
);
}
}
AppRegistry.registerComponent('SongGuessingGame', () => SongGuessingGame);
EXERCISE
Checkout the style-exercise
branch:
git fetech
git checkout style-exercise
Then get the ui to look like the mock up below:
To see a solution:
git fetch
git checkout style-exercise-solution
One of the core concepts of react native (and react for the web) is props and state.
- props - data that is passed to a component that cannot be changed by the component (read only)
- state - data that can be changed by a component
In practice, a prop is something that is passed into a component as an attribute. For example, the button has the props title, onPress, and color:
<Button
title="SIMPLE BUTTON"
onPress={() => null}
color="blue"/>
In contrast, state is owned by a component and may be changed by that component but by no other components. If other components need to read state information, the data should be passed to the child component as a prop.
A component's state is defined in the constructor. In the example, the state property color
is changed every 2 seconds to a new random color:
import React, { Component } from 'react'
import {Text} from 'react-native'
export default class RandomColor extends Component {
constructor(props) {
super(props)
this.state = {
color: 'red'
};
setInterval(() => {
const colors = ['red', 'green', 'blue', 'yellow', 'indigo'];
const color = colors[Math.floor(Math.random()*colors.length)];
this.setState({color});
}, 2000);
}
render() {
return <Text style={{color: this.state.color}}>Text Changes Colors</Text>
}
}
Let's create a customized button that we'll use throughout our app. The button will look similar to our mockup above:
git checkout custom-button-starter
We'll want to add some styling to the button so that it looks a little nicer. Let's add the following:
- borderRadius - rounded corners
- backgroundColor - the background of our button
- padding - padding around the text
- color - applied to the text component
- fontSize - applied to the text component
Also, our custom button needs to handle onPress
.
To see a solution:
git checkout custom-button-solution
EXERCISE
Using 3 components Welcome
, GameContainer
, and Guess
(plus the custom button we just created), make a UI in which the user sees the welcome screen first, then clicks a button to start the game. The game can just say hello world for now.
Your starter code is at:
git checkout start-game-exercise
It's important to pay attention to binding for this example.
And a solution is at:
git checkout start-game-solution
In the developer menu (Control + D on iOS simulator), there are lots of useful debugging tools
- Reload - does a full reload of the app. You can also do CMD + R on a mac
- Enable/Disable Remote Debugging - allows you to debug your app in chrome and set break points
- Enable/Disable Live Reload - automatically reloads your app whenever you change a file
- Enable/Disable Hot Reload - tries to swap out the code in one part of your app without a full reload. This can get confused. I typically use this when I'm styling
- Inspector - Allows you to see margin, padding, size, etc for components.
Throughout the lifetime of your react native component, you may want to run code on specific events. For our app, we will use:
- componentDidMount - common for making http requests
- componentWillUnMount - used to do cleanup. In our case, we will use it to release our playing song resource
- shouldComponentUpdate - used to optimize re-renders. Typically react is very good at this, so only do this when you are aware that there is an issue.
You can see the complete list of lifecycle events in the react docs.
There are many 3rd party npm modules to choose from for react-native. Many of them take advantage of native components. To add those components as a dependency, you will usually need to link. Let's add react-native-fs
and react-native-sound
for our song guessing game:
npm install --save react-native-fs react-native-sound
This command has downloaded the javascript that our react components will depend on, but the native libraries still need to be linked into our app. To link the libararies:
react-native link
We will be using the itunes affiliate api to download music. Below is a preset list of song. Create a file called SongData.js
and add this class:
export default class SongData {
static randomSongId() {
const songIds = ["995535015", "966411602", "823593456", "956689796", "943946671",
"982388023", "907242704", "201281527", "656801339", "910038357",
"250038575", "878000348", "794095205", "1645339", "400835962",
"325618", "169003415", "51958108", "192688540", "684811768",
"344799464", "217633921", "192811017", "71068886", "640047583",
"517438248", "656479859", "310237", "991390352", "344799727",
"162337613", "121695005", "159293848", "305118379",
"1193701392" ];
const id = Math.floor(Math.random() * songIds.length);
return songIds[id];
}
}
Notice a static method is used here just for fun. It allows us to call SongData.randomSongId()
without having to create an instance of SongData
.
To get JSON data for a song, we need to make a GET request to https://itunes.apple.com/us/lookup?id=<SONGID>
. In react-native, the fetch
api is used, which is a javascript standard! To use fetch, we need to understand promises:
const songId = SongData.randomSongId();
fetch(`https://itunes.apple.com/us/lookup?id=${songId}`) // returns promise
.then(d => d.json()) // gives back an object which we need to convert to json.
.then((d) => {
console.warn(d.results[0]);
return song; // returning the song for the next .then method
});
EXERCISE
Use the fetch
api and RNFS.downloadFile
(docs can be found here) to make a GET request to get the song data, then anther GET request to download the audio file. Once the audio file is download, display the artist and track info. The information that we want from the itunes api is:
- artist
- id
- album
- trackName
- audioUrl
Which lifecycle method would be appropriate for this request?
You can find the solution at:
git checkout fetch-song-solution
Now, let's take a second to talk about navigation...
Navigation has been a bit of a community pain point for react native. Here is a survey of the options:
- NavigatorIOS - part of react-native. Only supports iOS and doesn't seem to be in future plans NOT RECOMENDED
- Navigator - Also part of react-native. Implemented in JS. Also seems out of favor. NOT RECOMENDED
- NavigationExperimental - Part of react-native. A popular choice if you are adding redux to your app. Not recommended if you do not use redux.
- react-native-navigation - A native implementation of navigation from wix. If you want native components, this is a good bet.
- ex-navigation - Exponent's navigation library. Popular but not being actively supported. Instead...
- react-navigation - A new project supported by exponent and facebook. The plan is for react-navigation to replace NavigationExperimental. Check it out at https://reactnavigation.org/
Offers built in navigators:
- StackNavigator
- TabNavigator
Once your components are inside of a navigator, each component is given this.props.navigation
From this.props.navigation
, you get access to this.props.navigation.navigate
, which allows you to programmatically switch between stacks or tabs. Let's add a StackNavigator
to our app.
First, install react-navigation:
npm install --save react-navigation
Next, rewrite GameContainer
:
import React, { Component } from 'react';
import Welcome from './Welcome';
import Guess from './Guess';
import {StackNavigator} from 'react-navigation';
const GameContainer = StackNavigator({
Welcome: { screen: Welcome },
Guess: { screen: Guess }
}, {
initialRouteName: "Welcome"
});
export default GameContainer;
Next, inside of Welcome
, we need to use this.props.navigation
to change the screen:
render() {
const { navigate } = this.props.navigation;
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Guess That Song
</Text>
<CustomButton
onPress={() => navigate('Guess')}
title="Start" />
</View>
);
}
That is great! We can add a title if we like by adding navigationOptions
. Inside the class, add:
static navigationOptions = {
title: 'Guess That Song'
}
If we don't actually like the header in the stack view, we can get rid of it as well:
import React, { Component } from 'react';
import Welcome from './Welcome';
import Guess from './Guess';
import {StackNavigator} from 'react-navigation';
const GameContainer = StackNavigator({
Welcome: { screen: Welcome },
Guess: { screen: Guess }
}, {
initialRouteName: "Welcome",
headerMode: "none"
});
export default GameContainer;
See the solution at git checkout first-navigator-solution
To play our sound and get the MVP of our game working, let's create 3 new components: Answer
, GuessInput
, and PlayHint
:
Let's start with GuessInput
, its job is to present the user with a input and a button that says guess. It will be a child component of Guess
and it will relay change events to the parent component.
Here is some suggested styling for Guess
:
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
flashMessage: {
flex: 0.4,
justifyContent: 'center'
},
flashMessageText: {
color: "red",
fontSize: 30,
},
guessInput: {
flex: 1,
justifyContent: 'flex-start',
alignItems: 'center'
}
});
We will get to some of the style sheets later, for now, you will be using container and guessInput objects.
Here is the style for GuessInput
:
const styles = StyleSheet.create({
guess: {
backgroundColor: '#ededed',
height: 50,
width: 250,
marginBottom: 15
}
});
IMPORTANT: guess is the style sheet object for a TextInput
. On iOS, text inputs must set a height or they will not show up.
A TextInput
component has two props that we need to provide: value
and onChangeText
. The facebook docs are TextInput
are here
EXERCISE
Implement a working GuessInput
component that updates the text in the TextInput
as the user types. Also, the GUESS button, should call a function provided as a prop from the Guess
component to notify the parent that the guess has been submitted.
Solution at:
git checkout guess-input-solution
Next, let's make a component called PlayHint
. Its job will be to play the downloaded audio as long as there is audio left to play or if the user makes the correct guess.
QUESTION
Check out the docs for react-native-sound. How should we manage the state of the audio playing? Keep in mind that we have to call release
to clean up the playing audio.
Managing state that leaks out into native components is hard. There is no easy solution. In this case, I chose to not keep any state in the component. Rather, I just make sure to clean up when the component is finished. Here is PlayHint
:
import React, {Component} from 'react';
import { View } from 'react-native';
import Sound from 'react-native-sound';
export default class PlayHint extends Component {
constructor(props) {
super(props);
this.sound = null;
this.playCallback = this.playCallback.bind(this);
}
// We never want this component to be
// rerendered
shouldComponentUpdate() {
return false;
}
// Method for keeping track of when the song is done.
// Call onSongDone from the passed in props
playCallback(success) {
if (success) {
this.props.onSongDone();
} else {
console.warn("Song did not play properly");
}
}
// Start playing the song as soon as the component is
// mounted. This only happens 1 time.
componentDidMount() {
if (this.sound) {
this.sound.stop();
this.sound.release();
}
this.sound = new Sound(this.props.audioFile,
this.props.audioPath,
(e) => {
if (e) {
console.warn('error', e);
} else {
this.sound.play(this.playCallback);
}
});
}
// Make sure to clean up the native audio before
// the component goes away
componentWillUnmount() {
if (this.sound) {
this.sound.stop();
this.sound.release();
}
}
// We don't actually need to show anything for this
render() {
return (
<View />
);
}
}
The code is added in the following branch:
git checkout play-sound-starter
EXERCISE
Now that we have a audio playing component, use it! The Guess
component should add the PlayHint
component to the render method but only if the audio is ready to play. Do a console.warn
to tell the user if the song was guessed correctly or not. I am keeping the following state is Guess
:
this.state = {
playAudio: false,
guess: '',
guessIncorrect: undefined,
song: {}
}
I also have the following methods:
this.downloadAndPlaySong = this.downloadAndPlaySong.bind(this);
this.onSongDone = this.onSongDone.bind(this);
this.onChangeGuess = this.onChangeGuess.bind(this);
this.onGuess = this.onGuess.bind(this);
You will also need a method to check to see if your guess is correct. Here is what I did:
verifyGuess(guess) {
const guesses = guess.trim().toLowerCase().split(/\s+/);
const answers = `${this.state.song.artist} ${this.state.song.trackName}`
.trim().toLowerCase();
return guesses.reduce(function(acc, g) {
if (answers.indexOf(g) >= 0) {
return true;
}
return acc;
}, false);
}
Put these components together so that we can finally here some music!
Solution at:
git checkout play-sound-solution
Let's say we want our UI to show a navigation header when we are shown the answer. The only problem is that we told our StackNavigator not to show a header. The solution: sub navigators.
Inside of GameContainer
we can make another navigator for the Answer then use it within our primary navigator:
const AnswerNavigator = StackNavigator({
Answer: { screen: Answer}
}, {
initialRouteName: 'Answer'
});
const GameContainer = StackNavigator({
Welcome: {
screen: GameWelcome,
},
GuessView: {
screen: GuessView
},
AnswerNavigator: {
screen: AnswerNavigator
}
}, {
headerMode: 'none',
initialRouteName: "Welcome"
});
export default GameContainer;
But now we have another problem, we need to pass data to our answer component. Normally, if the component is in the same navigator, we can use navigate and params:
this.props.navigation.navigate('Answer', {artist: 'Tim'})
Then inside of our Answer component, we could get those params this way:
const artist = this.props.navigation.state.params.artist;
BUT our component isn't in the same navigator, so we have to use a NavigationAction
. Here is how it would work in our song game:
showAnswer() {
const { artist, trackName, album } = this.state.song;
const { guessIncorrect } = this.state;
const navigationAction = NavigationActions.navigate({
routeName: 'AnswerNavigator',
params: {},
// navigate can have a nested navigate action that will
// be run inside the child router
action: NavigationActions.navigate({
routeName: 'Answer',
params: {artist, trackName, album, guessIncorrect}
})
})
this.props.navigation.dispatch(navigationAction)
}
EXERCISE
Here is the styling and the view for Answer
. Try to figure out how to pass the correct params to Answer
from Guess
:
import React, {Component} from 'react'
import {
View,
Text,
Button,
Image,
TouchableOpacity,
StyleSheet
} from 'react-native'
import CustomButton from './CustomButton'
export default class Answer extends Component {
static navigationOptions = {
title: "Guess That Song"
}
render() {
const result = guessIncorrect === false ?
<Text style={styles.correctText}>Correct π</Text> :
<Text style={styles.incorrectText}>Incorrect π</Text>;
return (
<View style={styles.container}>
<View style={{marginBottom: 15}}>
<Text style={{fontSize: 25}}>{result}</Text>
</View>
<View style={styles.artistView}>
<Text style={styles.artistText}>{artist}</Text>
<Text style={styles.artistText}>{trackName}</Text>
<Text style={{fontSize:21}}>{album}</Text>
</View>
<Image
style={styles.img}
source={{uri: 'https://www.publicdomainpictures.net/pictures/130000/velka/musical-notes.jpg'}}
/>
<CustomButton
title={"CONTINUE"}
onPress={() => navigate('Guess')} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
artistText: {
fontSize: 25
},
artistView: {
marginBottom: 25,
alignItems: 'center',
},
img: {
height: 100,
width: 250,
marginBottom: 25,
},
correctText: {
fontSize: 25,
color: 'green',
marginBottom: 10
},
incorrectText: {
fontSize: 25,
color: 'red',
marginBottom: 10
},
nextButton: {
borderRadius: 4,
backgroundColor: "blue",
padding:10
},
buttonText: {
color: 'white',
fontSize: 25
}
});
Solution:
git checkout show-answer-solution