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:

format_list_bulleted
help_outline