Chapter 15: Post Production
Full Stack React Native
link Post Production
By the end of this lesson, developers will be able to:
- Publish their first Expo application
link The Story So Far
So far, we set up a React Native application and a GraphQL server. The app is designed to allow users to sign up, login and create votes and posts.
App Setup
git clone https://github.com/Maelstroms38/movies-starter.git
cd movies-starter
yarn
yarn start
Server Setup
git clone https://github.com/Maelstroms38/movies-api
cd movies-api
yarn
yarn db:reset
yarn start
Setting Up
Let's start by opening and running both projects in two separate windows.
cd <app-name>
yarn start
cd <server-name>
yarn start
Once you have confirmed both projects are running, we will add a client to the React Native project.
Heads Up! This lesson's code edits will only affect your React Native project.
link Apollo Client
The setup for this project builds from previous React Native clients. We will be using an authentication and subscription client.
Let's add any missing dependencies to support our Websocket schema.
yarn add apollo-link-ws subscriptions-transport-ws apollo-utilities
Now we can re-create the subscriptions client, with the following changes in our App.js
top level imports:
import { AppLoading } from 'expo';
import { Asset } from 'expo-asset';
import * as Font from 'expo-font';
import React, { useState } from 'react';
import {
Platform,
StatusBar,
StyleSheet,
View,
AsyncStorage
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import AppNavigator from './navigation/AppNavigator';
+import { split } from 'apollo-link';
import { ApolloClient } from 'apollo-client';
import { ApolloProvider } from '@apollo/react-hooks';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
+import { WebSocketLink } from 'apollo-link-ws';
+import { getMainDefinition } from 'apollo-utilities';
import { ApolloLink, Observable } from 'apollo-link';
Below the top level imports create the following constants that will connect to our websocket server:
const HTTP_URL = 'http://';
const HTTPS_URL = 'https://';
const WS_URL = 'ws://';
const WSS_URL = 'wss://';
const DEV_URL = 'localhost:4000/graphql';
const BASE_URL = 'HEROKU-APP-NAME.herokuapp.com/graphql';
// https://www.apollographql.com/docs/react/migrating/boost-migration/
const httpLink = new HttpLink({
uri: `${HTTP_URL}${DEV_URL}`,
credentials: 'include'
});
// Create a WebSocket link
const wsLink = new WebSocketLink({
uri: `${WS_URL}${DEV_URL}`,
options: {
lazy: true,
reconnect: true
}
});
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
Finally, include the refactored link
with your ApolloClient
:
const client = new ApolloClient({
- link: ApolloLink.from([requestLink, httpLink]),
+ link: ApolloLink.from([requestLink, link]),
cache: new InMemoryCache()
});
With that, we have an authentication and subscriptions client. Let's complete the posts integration with a refactored LinksScreen
!
link LinksScreen
We will be integrating the previous PostScreen
, replacing the contents of ./screens/LinksScreen.js
. Insert the following snippet:
import React, { useEffect } from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
TouchableOpacity,
ActivityIndicator
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
import Post from '../components/Post';
// Queries
const POSTS_QUERY = gql`
query {
posts {
id
title
link
imageUrl
author {
id
username
}
}
}
`;
// Subscriptions
const POSTS_SUBSCRIPTION = gql`
subscription {
postAdded {
id
title
link
imageUrl
author {
id
username
}
}
}
`;
const POST_UPDATED = gql`
subscription {
postEdited {
id
title
link
imageUrl
author {
id
username
}
}
}
`;
const POST_DELETED = gql`
subscription {
postDeleted
}
`;
export default function LinksScreen(props) {
const { navigation } = props;
const { subscribeToMore, loading, error, data } = useQuery(POSTS_QUERY, {
variables: {}
});
useEffect(() => {
subscribeToMore({
document: POSTS_SUBSCRIPTION,
updateQuery: (prev, { subscriptionData }) => {
console.log(subscriptionData);
if (!subscriptionData.data) return prev;
const newPostItem = subscriptionData.data.postAdded;
return Object.assign({}, prev, {
posts: [newPostItem, ...prev.posts]
});
}
});
subscribeToMore({
document: POST_UPDATED,
variables: {},
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev;
const editedPost = subscriptionData.data.postEdited;
const updatedPosts = [...prev.posts];
const updatedIndex = updatedPosts.findIndex(
post => post.id == editedPost.id
);
if (updatedIndex >= 0) {
updatedPosts[updatedIndex] = editedPost;
}
return Object.assign({}, prev, {
posts: updatedPosts
});
}
});
subscribeToMore({
document: POST_DELETED,
variables: {},
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev;
const deletedID = subscriptionData.data.postDeleted;
const updatedPosts = [...prev.posts];
const updatedIndex = updatedPosts.findIndex(
post => post.id == deletedID
);
updatedPosts.splice(updatedIndex, 1);
return Object.assign({}, prev, {
posts: updatedPosts
});
}
});
}, []);
if (loading) {
return (
<ActivityIndicator
color="#161616"
style={{ ...StyleSheet.absoluteFillObject }}
/>
);
}
const { posts } = data;
return (
<View style={styles.container}>
<FlatList
data={posts}
keyExtractor={(item, index) => {
return `${index}`;
}}
renderItem={(post, index) => {
const { item } = post;
return (
<Post
post={item}
onPress={() => navigation.navigate('LinkDetail', { post: item })}
/>
);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
justifyContent: 'space-around'
},
saveIcon: {
position: 'relative',
right: 20,
zIndex: 8
}
});
There are some minor revisions here, including a new LinkForm
navigation.
link LinkForm
The following screen represents one of the bonus challenge solutions from a previous section. Add a new file called ./screens/LinkForm.js
, with the following contents:
import React, { useState } from 'react';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
Dimensions,
TextInput,
Alert
} from 'react-native';
import RoundedButton from '../components/RoundedButton';
import { Ionicons } from '@expo/vector-icons';
import { Linking } from 'expo';
import { useMutation } from '@apollo/react-hooks';
import gql from 'graphql-tag';
const { width } = Dimensions.get('window');
// Mutations
const EDIT_POST = gql`
mutation EditPost(
$id: ID!
$title: String!
$link: String!
$imageUrl: String!
) {
editPost(id: $id, title: $title, link: $link, imageUrl: $imageUrl) {
id
title
link
imageUrl
}
}
`;
const ADD_POST = gql`
mutation AddPost($title: String!, $link: String!, $imageUrl: String!) {
addPost(title: $title, link: $link, imageUrl: $imageUrl)
}
`;
export default function LinkForm({ route, navigation }) {
const { params } = route;
const { post } = params || {};
const [addPost] = useMutation(ADD_POST);
const [editPost] = useMutation(EDIT_POST);
const [title, setTitle] = useState(post && post.title || '');
const [link, setLink] = useState(post && post.link || '');
const [imageUrl, setImageUrl] = useState(post && post.imageUrl || '');
return (
<View style={styles.container}>
<View>
<Text>Title</Text>
<TextInput
onChangeText={text => setTitle(text)}
value={title}
placeholder="Title"
autoCorrect={false}
autoCapitalize="none"
style={styles.input}
autoFocus
/>
</View>
<View>
<Text>Link</Text>
<TextInput
onChangeText={text => setLink(text)}
value={link}
placeholder="Link"
autoCorrect={false}
autoCapitalize="none"
style={styles.input}
/>
</View>
<View>
<Text>Image URL</Text>
<TextInput
onChangeText={text => setImageUrl(text)}
value={imageUrl}
placeholder="Image URL"
autoCorrect={false}
autoCapitalize="none"
style={styles.input}
/>
</View>
<RoundedButton
text={post ? "Edit Post" : "Add Post"}
textColor="#fff"
backgroundColor="rgba(75, 148, 214, 1)"
onPress={() => {
// TextInput validation
let nullValues = [];
if (!title) {
nullValues.push('Title');
}
if (!link) {
nullValues.push('Link');
}
if (!imageUrl) {
nullValues.push('Image URL');
}
if (nullValues.length) {
Alert.alert(`Please fill in ${nullValues.join(', ')}`);
} else {
if (post) {
editPost({ variables: { id: post.id, title, link, imageUrl } })
.then(() => {
navigation.navigate('LinkDetail', {
post: { id: post.id, title, link, imageUrl }
});
})
.catch(err => console.log(err));
} else {
addPost({ variables: { title, link, imageUrl } })
.then(() => {
navigation.goBack();
})
.catch(err => console.log(err));
}
}
}}
icon={
<Ionicons
name="md-checkmark-circle"
size={20}
color={'#fff'}
style={styles.saveIcon}
/>
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'flex-start',
alignItems: 'center',
paddingHorizontal: 15,
paddingVertical: 20
},
saveIcon: {
position: 'relative',
left: 20,
zIndex: 8
},
input: {
width: width - 40,
height: 40,
borderBottomColor: '#FFF',
borderBottomWidth: 1
},
image: {
width: width,
height: width,
resizeMode: 'cover'
}
});
link LinkDetail
Now that we have re-introduced LinkForm
, let's include a LinkDetail
too.
Create a new file called ./screens/LinkDetail
, and insert the following:
import React from 'react';
import { StyleSheet, Text, Image, ScrollView, View, Dimensions } from 'react-native';
import RoundedButton from '../components/RoundedButton';
import { useMutation } from '@apollo/react-hooks';
import { Ionicons } from '@expo/vector-icons';
import { Linking } from 'expo';
import gql from 'graphql-tag';
const { width } = Dimensions.get('window');
// Mutations
const DELETE_POST = gql`
mutation DeletePost($id: ID!) {
deletePost(id: $id)
}
`;
export default function LinkDetail({ route, navigation }) {
const [deletePost] = useMutation(DELETE_POST);
const { params } = route;
const { post } = params;
const { title, link, imageUrl } = post;
return (
<ScrollView>
<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}
/>
}
/>
<RoundedButton
text="Edit"
textColor="#fff"
backgroundColor="#a9a9a9"
onPress={() => {
navigation.navigate('LinkForm', { post });
}}
icon={
<Ionicons
name="md-options"
size={20}
color={'#fff'}
style={styles.saveIcon}
/>
}
/>
<RoundedButton
text="Delete"
textColor="#fff"
backgroundColor="#FA8072"
onPress={() => {
deletePost({ variables: { id: post.id } })
.then(() => navigation.goBack())
.catch(err => console.log(err));
}}
icon={
<Ionicons
name="md-trash"
size={20}
color={'#fff'}
style={styles.saveIcon}
/>
}
/>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'flex-start',
alignItems: 'center',
paddingHorizontal: 15
},
text: {
fontSize: 32,
color: '#161616',
padding: 15,
textAlign: 'center'
},
image: {
width: width,
height: width,
resizeMode: 'cover'
},
saveIcon: {
position: 'relative',
left: 20,
zIndex: 8
}
});
We simply renamed PostDetail
to LinkDetail
and PostForm
to LinkForm
.
link BottomTabNavigator
Only a few final revisions left! Let's add the following changes to our tab navigator ./navigation/BottomTabNavigator.js
top level imports:
import React from 'react';
import { Platform, AsyncStorage } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import TabBarIcon from '../components/TabBarIcon';
import HomeScreen from '../screens/HomeScreen';
import LinksScreen from '../screens/LinksScreen';
import SettingsScreen from '../screens/SettingsScreen';
import MovieDetail from '../screens/MovieDetail';
import LoginScreen from '../screens/LoginScreen';
import ProfileScreen from '../screens/ProfileScreen';
import AuthLoading from '../screens/AuthLoading';
+import LinkDetail from '../screens/LinkDetail';
+import LinkForm from '../screens/LinkForm';
import ProfileTab from './TopTabNavigator';
Now we can add our new LinkStack
inside ./navigation/BottomTabNavigator.js
:
const Stack = createStackNavigator();
function LinksStack() {
return (
<Stack.Navigator>
<Stack.Screen name="LinksScreen" component={LinksScreen} />
<Stack.Screen name="LinkDetail" component={LinkDetail} />
<Stack.Screen name="LinkForm" component={LinkForm} />
</Stack.Navigator>
)
}
const BottomTab = createBottomTabNavigator();
export default function BottomTabNavigator({ navigation, route }) {
// Set the header title on the parent stack navigator depending on the
// currently active tab. Learn more in the documentation:
// https://reactnavigation.org/docs/en/screen-options-resolution.html
navigation.setOptions({ headerShown: false });
return (
<BottomTab.Navigator initialRouteName={INITIAL_ROUTE_NAME}>
<BottomTab.Screen
name="Home"
component={HomeStack}
options={{
title: 'Now Playing',
tabBarIcon: ({ focused }) => (
<TabBarIcon focused={focused} name="md-film" />
)
}}
/>
<BottomTab.Screen
name="Links"
component={LinksStack}
options={{
title: 'Links',
tabBarIcon: ({ focused }) => (
<TabBarIcon focused={focused} name="md-link" />
)
}}
/>
<BottomTab.Screen
name="Profile"
component={ProfileStack}
options={{
title: 'Profile',
tabBarIcon: ({ focused }) => (
<TabBarIcon focused={focused} name="md-person" />
)
}}
/>
</BottomTab.Navigator>
);
}
Wrapping Up
Today we built an app that supports authentication, subscriptions and full CRUD! Now would be a great time to showcase our new product. Let's publish our new app using Expo Publish.
link Expo Publish
Before getting started, its important to note that this "published" app can be made available to the following test groups:
- Android devices with access to your build link
- iOS devices with your Expo account credentials
Publishing your expo project yields a link like https://exp.host/@username/an-example that (almost) anyone can load your project from.
expo publish
At this point you will be prompted to sign up for Expo. You can also sign in to an existing account.
After your app is successfully published, download the "Expo Client" app to your mobile device. Using the Expo Client, you can test your application on the go and show it off!
Congratulations on completing the course!
link Final Challenge
Create React Native App
It's time to put your new skills to the test - create an app with React Native and GraphQL! We hope you enjoyed the journey and look forward to seeing your next mobile app!