Chapter 7: App Navigator
Full Stack React Native
link Reddit App
Congratulations on completing unit two! Welcome to unit three, where we introduce full CRUD (Create, Read, Update, Destroy) and subscriptions.
Our project is a reddit-style social media app. It will include the following features:
- Browse a live feed of posts and images.
- Create posts, edit posts, delete posts.
- Subscribe to live posts updates.
By the end of this lesson, developers will be able to:
- Create custom App Navigator
- Create a reusable Post component
- Navigate to the Post Detail screen
Let's begin the bitcoins app with the Expo init
command. By now you should already have the expo-cli
installed globally.
expo init reddit-app
In the first prompt, be sure to select blank
:
? Choose a template:
----- Managed workflow -----
❯ blank a minimal app as clean as an empty canvas
blank (TypeScript) same as blank but with TypeScript configuration
tabs several example screens and tabs using react-navigation
In the second prompt, name your new reddit app.
If you followed these steps successfully, you should have a new project with the following folders:
.
├── App.js
├── app.json
├── assets
├── babel.config.js
├── node_modules
├── package.json
└── yarn.lock
link React Navigation Stack
React Navigation will allow us to create a navigation stack to transition between screens. Let's install their required dependencies:
yarn add @react-navigation/stack
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
For more info on
@react-navigation/stack
, click here.
link Posts Screen
The posts screen will be a list view of posts and images. We will use FlatList
to render posts data. Each row will be a pressable button, that brings users to the post details page.
Let's begin creating the Posts screen by adding the following code inside ./screens/Post.js
:
import React from 'react';
import { StyleSheet, Text, View, FlatList, TouchableOpacity, ActivityIndicator } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
const data = {
posts: [
{
"id": 1,
"title": "Hello World",
"link": "https://www.unsplash.com",
"imageUrl": "https://images.unsplash.com/photo-1484100356142-db6ab6244067?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1562&q=80"
}
]
}
export default function Posts(props) {
const { navigation } = props;
const { posts } = data;
return (
<View style={styles.container}>
<FlatList
data={posts}
keyExtractor={(item, index) => {
return `${index}`;
}}
renderItem={(post, index) => {
const { item } = post;
return (
<Text>{item.title}</Text>
);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
justifyContent: 'space-around'
},
});
link Stack Navigator
Until now, we used Expo to set up our app navigation. Let's set navigation up from scratch with this AppNavigator.js
:
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import Posts from '../screens/Posts';
const Stack = createStackNavigator();
export default function AppNavigator() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Posts" component={Posts} />
</Stack.Navigator>
</NavigationContainer>
);
}
For more info on
createStackNavigator
, click here.
link App Navigator
To see the Posts screen, we will render the AppNavigator.js
inside our parent App.js
component:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import AppNavigator from './AppNavigator';
export default function App() {
return (
<View style={styles.container}>
<AppNavigator />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
}
});
For more info on
createAppContainer
, click here.
Save and reload the simulator. The first post should be seen, which should read "Hello World."
link Post Component
Let's spruce up the list of Posts, with a new ./components/Post.js
component.
import React, { useState } from 'react';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
Image,
Dimensions
} from 'react-native';
const {width} = Dimensions.get('window');
export default function Post(props) {
const { post, navigation, onPress } = props;
const {
title,
link,
imageUrl,
} = post;
return (
<TouchableOpacity
style={styles.container}
onPress={() => {
onPress && onPress();
}}
>
{!!imageUrl && (
<View style={{flex: 2}}>
<Image
source={{uri: imageUrl}}
style={styles.image}
/>
</View>
)}
<View style={styles.footer}>
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
<Text style={styles.text} numberOfLines={1}>
{link}
</Text>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
width,
borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: '#dddddd',
},
image: {
flex: 2,
width,
height: width/2,
resizeMode: 'cover',
},
active: {
backgroundColor: 'rgba(255,255,255,0.05)',
},
footer: {
flex: 0.5,
alignItems: 'flex-start',
justifyContent: 'center',
paddingLeft: 10,
paddingVertical: 10
},
title: {
color: '#161616',
fontSize: 20,
},
text: {
color: '#161616',
fontSize: 16,
},
});
Save and import the new component inside Posts.js
:
import React from 'react';
import { StyleSheet, Text, View, FlatList, TouchableOpacity, ActivityIndicator } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
+ import Post from '../components/Post';
Replace the Text
component with the new Post
component:
export default function Posts(props) {
const { navigation } = props;
const { posts } = data;
return (
<View style={styles.container}>
<FlatList
data={posts}
keyExtractor={(item, index) => {
return `${index}`;
}}
renderItem={(post, index) => {
const { item } = post;
return (
- <Text>{item.title}</Text>
+ <Post post={item} onPress={navigation.navigate('Detail', {post: item})} />
);
}}
/>
</View>
);
}
link Post Detail
If you click any Post, nothing happens. Let's make a screen to display Post Details called ./screens/PostDetail.js
.
Prior to adding these two new mutations, let's bring over the RoundedButton
component from the previous unit.
Create a new file inside components
and paste in the RoundedButton
code.
Here's the RoundedButton source code, in case you need it.
import React from 'react';
import { Text, View, TouchableOpacity, StyleSheet } from 'react-native';
export default function RoundedButton(props) {
const { text, icon, textColor, backgroundColor, onPress } = props;
const color = textColor || 'white';
return (
<TouchableOpacity
onPress={() => onPress && onPress()}
style={[
styles.wrapper,
{ backgroundColor: backgroundColor || 'transparent' },
]}
>
<View style={styles.ButtonTextWrapper}>
{icon}
<Text style={[{ color }, styles.buttonText]}>{text}</Text>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
wrapper: {
padding: 15,
display: 'flex',
borderRadius: 40,
borderWidth: 1,
borderColor: 'white',
marginBottom: 15,
alignItems: 'center'
},
buttonText: {
fontSize: 16,
width: '100%',
textAlign: 'center'
},
ButtonTextWrapper: {
flexDirection: 'row',
justifyContent: 'flex-end'
}
});
With RoundedButton
back, we can make the final changes to PostDetail.js
:
import React from 'react';
import {
StyleSheet,
Text,
Image,
View,
Dimensions
} from 'react-native';
import RoundedButton from '../components/RoundedButton';
import { Ionicons } from '@expo/vector-icons';
import { Linking } from 'expo';
const {width} = Dimensions.get('window');
export default function PostDetail({ route }) {
const { params } = route;
const { post } = params;
const {
title,
link,
imageUrl,
} = post;
return (
<View style={styles.container}>
{!!imageUrl && <Image style={styles.image} source={{uri: imageUrl}} /> }
<Text style={styles.text}>{title}</Text>
<RoundedButton
text={`${link}`}
textColor="#fff"
backgroundColor="rgba(75, 148, 214, 1)"
onPress={() => {
Linking.openURL(link)
.catch(err => console.log(err));
}}
icon={<Ionicons name="md-link" size={20} color={"#fff"} style={styles.saveIcon} />}
/>
</View>
);
}
PostDetail.navigationOptions = screenProps => ({
title: "Post"
});
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'flex-start',
alignItems: 'center',
paddingHorizontal: 15,
},
text: {
fontSize: 32,
color: '#161616',
padding: 15,
},
image: {
width: width,
height: width,
resizeMode: 'cover',
},
saveIcon: {
position: 'relative',
left: 20,
zIndex: 8
},
});
link Navigate to PostDetail
Finally, we can include the PostDetail
screen inside AppNavigator.js
:
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { Ionicons } from '@expo/vector-icons';
import Posts from '../screens/Posts';
import Detail from '../screens/PostDetail';
const Stack = createStackNavigator();
export default function AppNavigator() {
return (
<Stack.Navigator>
<Stack.Screen name="Posts" component={Posts} options={{
headerRight: ({ navigation }) => (
<Ionicons
onPress={() => navigation.navigate('PostForm')}
name="md-add"
size={25}
color={"#161616"}
style={{
position: 'relative',
right: 20,
zIndex: 8
}} />
),
}} />
<Stack.Screen name="Detail" component={Detail} />
</Stack.Navigator>
)
};
Now when we click any Post, we navigate to that Post's detail screen. We can also click the "Link" button to see content in a mobile browser.
WELL DONE!
Wrapping Up
Today we utilized React Navigation and React Native to create the reddit starter app. Here's the full list of resources for this lecture: