Let’s Build an E-commerce App with ReactNative and Amplify
Click here to see the video walkthrough.
Table of Content
Introduction
Setting Up the Project
Adding Cognito Authentication
Adding AppSync API
Adding S3 Storage
Retrieving AppSync Data
Conclusion
01. Introduction
Hello! & Welcome to this complete guide on AWS Amplify and React Native. In this tutorial, we will build a simple e-commerce app. Users can log in/signup to this app. Users can add their own products, and all the users can view those products added by different users.
Here is a quick demo,
Even though this is not a very complex application, this will be a perfect starter project for you. We will use different AWS Services like S3 Storage, AWS AppSync API, and Amazon Cognito Authentication. Don’t worry I will explain these in detail, later.
Architecture Diagram
The following diagram demonstrates our AWS Architecture Diagram.
AWS Amplify makes it much easier to work with these different services. As always, our backend resources will be created and managed by Amplify. Let me explain what these services will do.
So, Amplify is the heart of our backend environment. Those arrows pointing from Amplify means that we will use Amplify to connect those different resources. Every product will have an image. We will store that image in an S3 Bucket. Product details will be saved in Amazon DynamoDB, a NoSQL database provided by AWS. To talk with that Database, we will use a GraphQL API provided by AWS AppSync. Amazon Cognito will handle authentication.
Ready to build the app? Let’s get started. 👷🏽♂️🧰
Prerequisites
To avoid any disturbances in the future, make sure you have the following prerequisites installed.
Node.js v10.x or later
npm v5.x or later
Amplify CLI (version @4.40.1, what I’m using in the tutorial)
ReactNatice CLI (version 2.0.1)
02. Setting Up the Project
Installing and Configuring Amplify CLI
Through this tutorial, we will work with AWS Amplify CLI. You can install it by running,
npm install -g @aws-amplify/cli@4.39.0
Then you need to run amplify configure
. This will set up your Amplify CLI. There you will set up a new IAM User. You will finish setting up your IAM User, by providing the accessKeyId
and secretAccessKey
for your IAM user.
If you are stuck at some point, you can refer to this original guideline on installing Amplify CLI, https://docs.amplify.aws/cli/start/install
Creating a New ReactNative Application
Hope you have installed and configured Amplify CLI.
To work with ReactNative, you will have to set up the Android development environment. You can refer to this original guide, https://reactnative.dev/docs/environment-setup
Let’s create a new React Native app called AmplifyShop.
npx react-native init amplify_shop
If you have already installed react-native-cli
, you can use that instead of npx
.
Open the newly created React Native Project using Android Studio. Open the Android Emulator using Android Studio’s AVD Manager. In the project directory, run these two commands.
npx react-native start
npx react-native run-android
Now, the React Native project should run on your Android Emulator. If you are stuck at some point, please refer to the guide that I have suggested earlier.
Initializing Amplify Backend
Let’s initialize Amplify for our project. Then we can add services one by one.
In the project directory, run
amplify init
Then you will be prompted for the following information regarding the project you initialize.
When you initialize your Amplify Project,
It creates a file called
aws-exports.js
in the src directory. This file will store all the relevant information to identify the AWS resources/services that will allocate in the future.It creates a directory called
amplify
. We will use this directory to store the templates and configuration details of the services that we will use in the future. In this directory, Amplify will hold our backend schema as well.It creates a Cloud Project. That project can be viewed using the
amplify console
command.
Next, we need to install all the necessary dependencies by running the following command.
npm install aws-amplify aws-amplify-react-native amazon-cognito-identity-js @react-native-community/netinfo
You will also need to install the pod dependencies for iOS.
npx pod-install
Configuring Amplify Backend
To complete setting up our Amplify project, we need to configure amplify in a higher-order component. Adding the following lines of code in your App.js
or index.js
file will do the job.
import Amplify from 'aws-amplify';
import awsconfig from './aws-exports';
Amplify.configure({
...awsconfig,
Analytics: {
disabled: true,
},
});
That completes setting up the project. Now let’s add those services one by one.
03. Adding Cognito Authentication
Now, adding Authentication to your React Native App never gets easier than Amplify.
Adding Sign-up and Log-in
Run amplify add auth
in your project directory. Submit the following information when configuring Authentication.
Then, run amplify push
, to deploy your backend changes. Amplify will take care of the rest by creating your Cognito Userpool.
The authentication UI component, provided by Amplify Framework, will provide the entire authentication flow.
In the App.js
file,
- Import
withAuthenticator
component
import { withAuthenticator } from 'aws-amplify-react-native'
- Wrap the main component with
withAuthenticator
component.
export default withAuthenticator(App)
When you run your app. This login screen will show up. Try logging in as a new user. This will lead you to the home page. The newly created user will be saved in our Cognito User Pool.
Before Adding AppSync API, let’s add navigation to our App.
Adding ReactNative Navigation
Our App will contain two screens. One Screen to display the list of products and the other to add new products. Let’s create these two screens.
Create a new directory called src
. In that directory, create a folder called screens
. In that folder src/screens
, create two new javascript files named add-product-screen.js
and home-screen.js
I just prefer this naming convention. You can use any convention.
Copy and paste the following sample code. Do change the function name (‘HomeScreen’ and ‘AddProductScreen’) and the title according to the page.
directory: src/screens/ home.js, add-product-screen.js
import React from 'react';
import {SafeAreaView, StatusBar, Text} from 'react-native';
const HomeScreen = (props) => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<Text>Home</Text>
</SafeAreaView>
</>
);
};
export default HomeScreen;
There are multiple ways to add navigation into ReactNative Apps. In this tutorial, we will use ‘Stack Navigator Library’ from React Navigation. First, we should install it using npm.
npm install @react-navigation/native
Install all the additional third-party dependencies as well.
npm install react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view @react-navigation/stack
From React Native 0.60 and higher, linking is automatic. So you don’t need to run react-native link
.
If you’re on a Mac and developing for iOS, you need to install the pods (via Cocoapods) to complete the linking.
npx pod-install ios
To finish installing React Native Navigation, add the following import in your App.js
or index.js
file.
import 'react-native-gesture-handler';
For the sake of this tutorial, I will use two additional styling libraries. I will use react-native-elements
and react-native-vector-icons
. Let’s install those using npm.
npm install react-native-elements
npm install react-native-vector-icons
In order to view fontawesome icons
, we need to add the following line into android/app/build.gradle file.
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
With that out of the way, move into App.js
file. We will use the App.js
file to set up navigation in our App. Replace the current code with the following.
directory: App.js
import React from 'react';
import {StyleSheet, View, TouchableOpacity} from 'react-native';
import {createStackNavigator} from '@react-navigation/stack';
import {NavigationContainer} from '@react-navigation/native';
import AddProductScreen from './src/screens/add-product-screen';
import HomeScreen from './src/screens/home-screen';
import {Button} from 'react-native-elements';
import Icon from 'react-native-vector-icons/FontAwesome';
import {withAuthenticator} from 'aws-amplify-react-native';
const App: () => React$Node = () => {
const Stack = createStackNavigator();
return (
<>
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={({navigation}) => ({
title: 'Home',
headerStyle: {
backgroundColor: '#ff9300',
},
headerRight: () => (
<TouchableOpacity
style={styles.addButton}
onPress={() => navigation.navigate('AddProduct')}>
<Icon name={'plus'} size={20} color="#000000" />
</TouchableOpacity>
),
})}
/>
<Stack.Screen
name="AddProduct"
buttonStyle={styles.addButton}
component={AddProductScreen}
options={{
title: 'Add Product',
headerStyle: {
backgroundColor: '#ff9300',
},
}}
/>
</Stack.Navigator>
</NavigationContainer>
</>
);
};
const styles = StyleSheet.create({
addButton: {
marginRight: 20,
},
logOutBtn: {
marginLeft: 10,
},
});
export default withAuthenticator(App);
This is the simplest and easiest way to add navigation. We got Stack.Navigator
Component, which we can provide an initial route. Inside that wrapper component, we can define each screen using the Stack.Screen
component.
We can use that options
prop to define the header for each screen. I just added a navigation button on the right side of our header. It should navigate to our AddProduct Screen.
Since we are using Stack Navigation, the new screen gets loaded on top of the previous screen. Therefore, the back button will be added automatically.
Adding Sign-Out Option
How about adding a sign-out option to our Home Screen. We are already passing headerRight
to our home screen. We can pass another prop called headerLeft
. This will create a new button on the left side of our header.
Do paste in the following code along with the import.
// importing Auth Class from Amplify Library
import {Auth} from 'aws-amplify';
headerLeft: () => (
<View style={styles.logOutBtn}>
<Button
icon={<Icon name="sign-out" size={25} color="#000000" />}
onPress={}
type="clear"
/>
</View>
),
Sign-out button will trigger, Auth.signOut()
method. This method will end the user’s login session. When the session is over, the login-screen gets loaded automatically. We don’t need to manage any state variable. Amplify will do the authentication session handling.
So, that’s it for Navigation. Learn more about React Native Navigation here. In the end, the result should be something like this.
04. Adding AppSync API
Let’s store details about products by adding an AppSync API. We will save details about products such as name, price, and description. We will also add an image to every product. Let’s keep that image option for later.
Executing ‘amplify add api’
As I’ve said earlier, through AppSync, we can build a GraphQL API. All the heavy lifting, such as connecting and creating DynamoDB tables, generation queries, and mutations, will be done by AppSync.
Let’s started by provisioning an AppSync API for our Amplify Backend. Execute,
amplify add api
and you will be prompted for the following information.
Just accept the defaults.
Editing GraphQL Schema
Let’s edit our schema. You will find our schema.graphql
file in amplify/backend/api/schema.graphql
directory. Copy and paste the following schema.
type Product
@model(subscriptions: null)
@auth(
rules: [
{ allow: owner },
{ allow: private, operations: [read] }
]) {
id: ID!
name: String!
description: String
price: Float
userId: String
userName: String
image: String
}
Save the file. Follow with an amplify push
to deploy your changes into AWS Cloud.
Now, our AppSync API has been created. Also, the AppSync Library automatically created queries, mutations for our GraphQL Schema. Run amplify api console
to view your AppSync API in AWS.
You could play around with some GraphQL operations in this AWS AppSync Console.
Adding AddProduct Screen
Let’s start interacting with our AppSync API.
Before that, I want to add an extra package that will help in creating a React Native Form. With the tcomb-form-native package, you can quickly create a form on the fly. So, let’s install it using npm.
npm install tcomb-form-native
Copy and paste the following code into our add-product-screen.js file.
directory: src/screens/add-product-screen.js
import React, {useState} from 'react';
import {StyleSheet, SafeAreaView, ScrollView} from 'react-native';
import {Button} from 'react-native-elements';
import t from 'tcomb-form-native';
const Form = t.form.Form;
const User = t.struct({
name: t.String,
price: t.Number,
description: t.String,
});
const AddProductScreen = ({navigation}) => {
const [form, setForm] = useState(null);
const [initialValues, setInitialValues] = useState({});
const options = {
auto: 'placeholders',
fields: {
description: {
multiLine: true,
stylesheet: {
...Form.stylesheet,
textbox: {
...Form.stylesheet.textbox,
normal: {
...Form.stylesheet.textbox.normal,
height: 100,
textAlignVertical: 'top',
},
},
},
},
},
};
const handleSubmit = async () => {
// Saving product details
};
return (
<>
<SafeAreaView style={styles.addProductView}>
<ScrollView>
<Form
ref={(c) => setForm(c)}
value={initialValues}
type={User}
options={options}
/>
<Button title="Save" onPress={handleSubmit} />
</ScrollView>
</SafeAreaView>
</>
);
};
const styles = StyleSheet.create({
addProductView: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
paddingTop: 15,
height: 'auto',
},
});
export default AddProductScreen;
Try running your app, you should see a form just like this.
Let’s inspect our code.
You can see that, I didn't use any textInputs
. I just defined our fields using t.struct
and only since the description is a multiple textInput
, we need to pass in extra options.
In our handleSubmit
function, we are saving entered details in the database. Paste in the following code inside our handleSubmit
function. Don’t forget the imports.
import { Auth, API, graphqlOperation} from 'aws-amplify';
import {createProduct} from '../../graphql/mutations';
try {
const value = await form.getValue();
const user = await Auth.currentAuthenticatedUser();
const response = await API.graphql(
graphqlOperation(createProduct, {
input: {
name: value.name,
price: value.price.toFixed(2),
description: value.description,
userId: user.attributes.sub,
userName: user.username,
},
}),
);
console.log('Response :\n');
console.log(response);
} catch (e) {
console.log(e.message);
}
Auth.currentAuthenticatedUser()
will do exactly what the name suggests. It will return details about the logged-in user. Cognito gives every user an attribute called sub
, a unique string value. We will save that as the userId assigned to a product. Username will showcase the product owner.
The createProduct
mutation was generated automatically. Here we are referring to that mutation, which was defined in graphql/mutations.js
file.
Now, after running the app and saving a product, you should see a console.log of the response. You could also query in the AWS AppSync Console.
05. Adding S3 Storage
Now by far, users can save product details. We should also add an extra option to upload a product image. We will need an S3 Bucket to store product images. Working with S3 really gets easier with Amplify. Let me show you.
Before that, install the React Native image picker library.
npm install react-native-image-picker
RN >= 0.60
cd ios && pod installRN < 0.60
react-native link react-native-image-picker
For now, this image picker library will support only 21 or newer SDK Versions. So edit minSDK version in android/build.gradle file.
buildToolsVersion = "29.0.2"
minSdkVersion = 21
compileSdkVersion = 29
targetSdkVersion = 29
Executing ‘amplify add storage’
Run,
amplify add storage
to create a new S3 Bucket. Accept the defaults in the prompt.
Run amplify push
, to deploy your changes.
Updating the Form
Let’s add image uploading and previewing options into our form. I build a fancy ImageUploader Component with an image preview. Make sure to add that by making a new components
directory in the src
folder.
directory: src/components/ImageUploader.js
import React from 'react';
import {View, Image, Button, StyleSheet} from 'react-native';
const ImageUploader = ({handleChoosePhoto, photo}) => {
return (
<View style={styles.imageView}>
{photo && <Image source={{uri: photo.uri}} style={styles.photo} />}
<Button
style={styles.photoBtn}
title="Choose Photo"
onPress={handleChoosePhoto}
/>
</View>
);
};
const styles = StyleSheet.create({
imageView: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
paddingBottom: 15,
},
photo: {
width: 200,
height: 200,
},
});
export default ImageUploader;
In order to use this image uploading option, we will make the following changes in our add-product-screen.js
file.
We will add the extra ImageUploader Component inside our ScrollView Component.
return (
<>
<SafeAreaView style={styles.addProductView}>
<ScrollView>
<Form
ref={(c) => setForm(c)}
value={initialValues}
type={User}
options={options}
/>
<ImageUploader photo={photo} handleChoosePhoto={handleChoosePhoto} />
<Button title="Save" onPress={handleSubmit} />
</ScrollView>
</SafeAreaView>
</>
);
Declare this state variable along with the new handleChoosePhoto function.
const [photo, setPhoto] = useState(null);
const handleChoosePhoto = async () => {
const product = await form.getValue();
setInitialValues({
name: product.name,
price: product.price,
description: product.description,
});
await launchImageLibrary({}, (response) => {
// console.log(response.data);
if (response.uri) {
console.log('Photo Extension: \n');
// console.log(response);
setPhoto(response);
}
});
};
If we don’t set initial values, launching the image library will reset the form.
Don’t forget to add these imports as well.
import {launchImageLibrary} from 'react-native-image-picker';
import {Storage} from 'aws-amplify';
You can do a test run of the form. You should see something like this.
Also, we should update our handleSubmit
function.
const handleSubmit = async () => {
try {
const value = await form.getValue();
console.log('value: ', value);
const user = await Auth.currentAuthenticatedUser();
if (photo) {
const response = await fetch(photo.uri);
const blob = await response.blob();
console.log('FileName: \n');
await Storage.put(photo.fileName, blob, {
contentType: 'image/jpeg',
});
}
const response = await API.graphql(
graphqlOperation(createProduct, {
input: {
name: value.name,
price: value.price.toFixed(2),
description: value.description,
userId: user.attributes.sub,
userName: user.username,
image: photo.fileName,
},
}),
);
console.log('Response :\n');
console.log(response);
navigation.navigate('Home');
} catch (e) {
console.log(e.message);
}
};
We can upload an S3 Image into our bucket using Storage.put
method, provided by AWS Amplify Library. We need our file name (image key in S3 ) to access our file again. So we will store that in our database.
Try uploading a new image. Submit the form. Wait until the image uploads. You should see a console.log like this.
[Sat Jan 02 2021 01:58:21.981] LOG Response :
[Sat Jan 02 2021 01:58:21.982] LOG {"data": {"createProduct": {"createdAt": "2021-01-01T20:28:22.382Z", "description": "About Sahan New Product", "id": "f3188508-5ee7-4af4-acf3-3c948f61d868", "image": "6ca2947e-766b-445e-b260-0041502e652a", "name": "Sahan New Product", "price": 200, "updatedAt": "2021-01-01T20:28:22.382Z", "userId": "7d5fa0a3-4d26-4354-8028-7cc597a69447", "userName": "sahan"}}}
06. Retrieving AppSync Data
Now, let’s show a Product List View on our home screen. For that, I have created two new components,
- ProductCard Component
directory: src/components/ProductCard.js
import React, {useEffect, useState} from 'react';
import {Text, StyleSheet, View} from 'react-native';
import {Card, Icon, Image} from 'react-native-elements';
import {Storage} from 'aws-amplify';
const ProductCard = ({
productName,
productOwner,
productPrice,
productImage,
}) => {
const [imageSource, setImageSource] = useState(null);
const getImage = async () => {
try {
const imageURL = await Storage.get(productImage);
setImageSource({
uri: imageURL,
});
} catch (e) {
console.log(e);
}
};
useEffect(() => {
getImage();
}, []);
return (
<Card containerStyle={styles.cardContainer}>
<Card.Title style={styles.cardTitle}>{productName}</Card.Title>
<Card.Divider />
{imageSource && (
<Image source={imageSource} style={styles.productImage} />
)}
{!imageSource && (
<View style={styles.altView}>
<Text>Product Image</Text>
</View>
)}
<Text style={styles.productPrice}>{productPrice}$</Text>
<View style={styles.ownerTitle}>
<Icon name="person-pin" />
<Text style={styles.productOwner}>{productOwner}</Text>
</View>
</Card>
);
};
const styles = StyleSheet.create({
cardContainer: {
marginBottom: 20,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
productImage: {
width: 200,
height: 200,
alignSelf: 'center',
},
productPrice: {
marginTop: 10,
marginBottom: 10,
fontSize: 16,
fontWeight: 'bold',
},
altView: {
width: 200,
height: 200,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
},
cardTitle: {
fontSize: 20,
},
productOwner: {
fontSize: 16,
fontWeight: 'bold',
alignSelf: 'center',
},
ownerTitle: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
},
});
export default ProductCard;
- ProductList Component
directory: src/components/ProductList.js
import React from 'react';
import {View, Text, FlatList, StyleSheet, RefreshControl} from 'react-native';
import ProductCard from './ProductCard';
const ProductList = ({productList, refreshing, onRefresh}) => {
return (
<View style={styles.productsView}>
{productList && (
<FlatList
style={styles.productList}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
keyExtractor={(item) => item.id}
data={productList}
renderItem={({item}) => {
return (
<ProductCard
productName={item.name}
productImage={item.image}
productOwner={item.userName}
productPrice={item.price}
/>
);
}}
/>
)}
</View>
);
};
const styles = StyleSheet.create({
itemText: {
fontSize: 15,
},
productText: {
fontSize: 20,
fontWeight: 'bold',
alignSelf: 'center',
},
productsView: {
padding: 0,
margin: 0,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
},
productList: {
padding: 5,
marginBottom: 20,
},
});
export default ProductList;
Now, let’s use this ProductList Component in the Home Screen. Replace the current sample code with the following code.
directory: src/screens/home-screen.js
import React, {useEffect, useState} from 'react';
import {API} from 'aws-amplify';
import {SafeAreaView, StatusBar, TouchableOpacity} from 'react-native';
import {listProducts} from '../../graphql/queries';
import ProductList from '../components/ProductList';
const HomeScreen = (props) => {
const [productsList, setProducts] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const fetchProducts = async () => {
try {
const products = await API.graphql({query: listProducts});
if (products.data.listProducts) {
console.log('Products: \n');
console.log(products);
setProducts(products.data.listProducts.items);
}
} catch (e) {
console.log(e.message);
}
};
useEffect(() => {
fetchProducts();
}, []);
const onRefresh = async () => {
setRefreshing(true);
await fetchProducts();
setRefreshing(false);
};
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
{productsList && (
<ProductList
productList={productsList}
refreshing={refreshing}
onRefresh={onRefresh}
/>
)}
</SafeAreaView>
</>
);
};
export default HomeScreen;
In the useEffect hook of our Home Screen, we are fetching all products. This time, we are running a GraphQL query listProducts
, which will be defined automatically in the graphql/queries.js
file.
We are passing those fetched products, into our ProductList Component. ProductList Component will render a ProductCard for each product.
In PtoductCard Component, when we pass in the image filename to Storage.get
function, we will get the full image URL.
Try running your app, you should now see your product list.
07. Conclusion
With that, we were able to complete all our functionalities successfully. How about letting users order products? I will save that for you to try on 😃.
Congratulations on Completing the Tutorial! 🎉
If we recap on what we have done,
We added Cognito Authentication to let users log-in or sign-up to our app.
Additionally, we included Navigation and Sign-out options.
We created the AppSync GraphQL API, and we saved some product details in our Database.
We created S3 Bucket to let users upload an image to each product.
At the Home Screen, we were able to show a product ListView to the user.
I think now you have a good understanding of working with these different AWS resources in your ReactNative Apps.
I hope you have completed all the steps without running into any issues. However, if you do, you can ask anything in the comments section below.
Video Walkthrough related to this BlogPost:
Aws Amplify and React Native Crash Course
Top comments (2)
Sir, I am getting error
ERROR TypeError: Cannot read property 'configure' of undefined, js engine: hermes
LOG Running "locationalert" with {"rootTag":11}
ERROR Invariant Violation: "locationalert" has not been registered. This can happen if:
AppRegistry.registerComponent
wasn't called., js engine: hermesThis is error is after amplify add auth, when I start my react-native app
Hi, it was excellent article. my AWS auth login UX has been loaded but from login page when i am clicking on signUP i am getting Invariant Violation: Picker has been removed from React Native. It can now be installed and imported from '@react-native-picker/picker' instead of 'react-native' this error. can you please help me out. i am trying with fresh project