DEV Community

Christian Nwamba
Christian Nwamba

Posted on

Build fast React Native apps with AWS Amplify

Overview

In this article, we will be building a mobile application with React Native using Expo SQLite adapter as a storage adapter for DataStore.

By the end of the of article, you will be able to build a contact app using the SQLite adapter for storage as shown in the video below:

Expo enables developers to create hybrid JavaScript applications from the comfort of their terminals. Expo has an adapter that gives your app access to a database that can be queried through a WebSQL-like API called expo-sqlite. The database is persisted across restarts of your app.

Datastore allows you to interact with your data offline first, and it has a sync engine behind the scenes to send data to the backend. DataStore provides a model for using distributed or shared data without writing any additional code for implementing online and offline states. This programming model ensures that working with distributed or shared data is as easy as working with local data. DataStore provides data modeling using GraphQL and converts it to models that you can use in building JavaScript, Android, iOS, and Flutter applications.

With DataStore, at runtime models are passed into a Storage Engine that has a Storage Adapter. Amplify ships with default Storage Adapter implementations, such as SQLite and IndexedDB.

Pre-requisites

  • Nodejs ≥v14 installed
  • Knowledge of JavaScript and React
  • AWS Amplify CLI installed
npm install -g @aws-amplify/cli
Enter fullscreen mode Exit fullscreen mode
  • AWS Amplify configured
amplify configure
Enter fullscreen mode Exit fullscreen mode
  • Expo CLI installed
npm install -g expo-cli
Enter fullscreen mode Exit fullscreen mode
  • Expo Go (installed from your mobile playstore)

Building the React Native app with Expo SQLite and Datastore

Let’s get started by initializing a new Expo app. Run this command on your terminal

expo init ReactAmplifyDataStoreExpo
Enter fullscreen mode Exit fullscreen mode

This will create a new React Native application. Change directory and initialize amplify on the directory with these commands

cd AmplifyDataStoreExpo
npx amplify-app@latest
Enter fullscreen mode Exit fullscreen mode

Installing Dependencies

Next, install the following dependencies with expo

expo install aws-amplify @aws-amplify/datastore-storage-adapter expo-sqlite expo-file-system @react-native-community/netinfo @react-native-async-storage/async-storage
Enter fullscreen mode Exit fullscreen mode
  • aws-amplify: this package will enable us to build our AWS cloud-enabled mobile application.
  • @aws-amplify/datastore-storage-adapter: this is the SQLite storage adapter for Amplify Datastore
  • expo-sqlite: this package gives the app access to a database that can be queried through a WebSQL-like API.
  • expo-file-system: This package provides access to the local file system on the device.
  • @react-native-community/netinfo: this package is the React Native Network Info API for Android, iOS, macOS, Windows & Web. It allows you to get information on Connection type and Connection quality
  • @react-native-async-storage/async-storage: This is an asynchronous, unencrypted, persistent, key-value storage system for React Native.

Next, we go on to create the model for our application.

Generating model with Amplify

The first step to building an app backed by datastore is by defining a schema. The schema contains data types and relationships that represent the app's functionality. In our app, we will create a schema for a simple contact application.
Head over to amplify/backend/api/schema.graphql, delete the content, and add these lines of code.

type Contact @model {
  id: ID!
  name: String!
  phone: String!
  email: String
}
Enter fullscreen mode Exit fullscreen mode

Here, our Contact model has id, name, phone, and email as properties. Let’s now go ahead to create the graphql generated schema and models. Run this command on your terminal

npm run amplify-modelgen
Enter fullscreen mode Exit fullscreen mode

This will create an src/models folder with the model and graphql schema. We are making progress!

Next, we initialize the amplify backend. Run this command on your terminal:

amplify init
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile
Enter fullscreen mode Exit fullscreen mode

This will also create an aws-exports.js in the src directory. Awesome.
Next, we go ahead to deploy the amplify backend to the cloud. Run this command to do that:

amplify push
Enter fullscreen mode Exit fullscreen mode

For this option, ? Do you want to generate code for your newly created GraphQL API No, choose No. We are choosing no here because we’ll be using the datastore API. This will take some time to deploy, depending on your network speed.

Importing Dependencies

Navigate to App.js, delete the content and add these lines of code:

import { DataStore } from 'aws-amplify';
import { ExpoSQLiteAdapter } from '@aws-amplify/datastore-storage-adapter/ExpoSQLiteAdapter';
import Amplify from '@aws-amplify/core';
import config from './src/aws-exports'
import React, { useState, useEffect } from 'react'
import { Text, View, TextInput, Button } from 'react-native'
import { Contact } from './src/models'
Enter fullscreen mode Exit fullscreen mode

Here, we imported DataStore from amplify, we also got the ExpoSQLiteAdapter from amplify datastore storage adapter package and the Contact model from models.

Configuring Amplify and DataStore with ExpoSQLiteAdapter

Next, add these lines of code to App.js:

Amplify.configure(config)

DataStore.configure({
  storageAdapter: ExpoSQLiteAdapter
});
Enter fullscreen mode Exit fullscreen mode

Here, we configure Amplify with config from aws-exports, and then we set ExpoSQLiteAdapter as the storageAdapter for Datastore. Let’s move on.

Implementing UI Inputs

  //App.js
  const initialState = { name: '', email: '', phone: '' }

  function App() {
    return(
      <View style={container}>
        <Text style={heading}> My Contacts </Text>
        <TextInput
          onChangeText={v => onChangeText('name', v)}
          placeholder='Contact name'
          value={formState.name}
          style={input}
        />
        <TextInput
          onChangeText={v => onChangeText('email', v)}
          placeholder='Contact email'
          value={formState.email}
          style={input}
        />
        <TextInput
          onChangeText={v => onChangeText('phone', v)}
          placeholder='Contact phone'
          value={formState.phone}
          style={input}
        />
        <Button onPress={createContact} title='Create Contact' />
    </View>
  )
}
Enter fullscreen mode Exit fullscreen mode

Here, we start by setting the initial state of the input fields. We also have TextInput fields and a Button that invokes the createContact function onPress. Let’s go ahead and create the createContact function.

Implementing Create Contact Functionality

Add these lines of code to App.js


const [formState, updateFormState] = useState(initialState)

async function createContact() {
    if (!formState.name && !formState.email && !formState.phone) return
    await DataStore.save(new Contact({ ...formState }))
    updateFormState(initialState)
  }
Enter fullscreen mode Exit fullscreen mode

Then, we have two state variables, the first formState will get the values of the input fields and updateFormState will update the state when the onChangeText event is triggered.
Next, we have a createContact function. First, we have a condition that validates if the name, email, and phone fields are empty, if they are, the function returns false, else, we go on to save the field values to Datastore.

Implementing View Contact Functionality

Add these lines of code to App.js

  const [contacts, updateContacts] = useState([])

  async function fetchContacts() {
    const contacts = await DataStore.query(Contact)
    updateContacts(contacts)
  }
Enter fullscreen mode Exit fullscreen mode

The other variable is an array that holds the contacts we will be fetching from DataStore. Next, we have a fetchContacts function that queries the Contact model and then we update the contacts array.

  {
    contacts.map(contact => (
      <View key={contact.id}>
        <View style={contactBg}>
          <Text style={contactText}>Name: {contact.name}</Text>
          <Text style={contactText}>Email: {contact.email}</Text>
          <Text style={contactText}>Phone: {contact.phone}</Text>
        </View>
      </View>
    ))
  }
Enter fullscreen mode Exit fullscreen mode

Here, we mapped through the contacts array to render the contact name, email, and phone.

Implementing onChangeText functionality

Add these lines of code to App.js

function onChangeText(key, value) {
    updateFormState({ ...formState, [key]: value })
}
Enter fullscreen mode Exit fullscreen mode

When the onChangeText event is triggered the formState is updated with the field values.

Implementing Subscriptions with DataStore

Add these lines of code to App.js

useEffect(() => {
  fetchContacts()
  const subscription = DataStore.observe(Contact).subscribe(() => fetchContacts())
  return () => subscription.unsubscribe()
})
Enter fullscreen mode Exit fullscreen mode

Finally, we have a useEffect() where we call the fetchContacts(), and then we do something interesting. We create a graphql subscription with Datastore. So if you’re updating the contacts on the web, or iOS app, or Android, you can see those updates in real-time. Awesome.

Adding some Styles

We should go ahead to add the styles. Add the lines of code to the bottom of the codebase in App.js

const container = { padding: 20, paddingTop: 80 }
const input = { marginBottom: 10, padding: 7, backgroundColor: '#ddd' }
const heading = { fontWeight: 'normal', fontSize: 40 }
const contactBg = { backgroundColor: 'white' }
const contactText = { margin: 0, padding: 9, fontSize: 20 }

export default App
Enter fullscreen mode Exit fullscreen mode

Awesome!

Now let’s go ahead and run the app. Run this command on your terminal.

expo start
Enter fullscreen mode Exit fullscreen mode

You should have something like this in the terminal


➜  ReactAmplifyDataStoreExpo git:(master) ✗ expo start

This command is being executed with the global Expo CLI.
To use the local CLI instead (recommended in SDK 46 and higher), run:
› npx expo start

Starting project at /Users/mac/Desktop/sammy/ReactAmplifyDataStoreExpo
Starting Metro Bundler
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
█ ▄▄▄▄▄ █▀▄█▀     █ ▄▄▄▄▄ █
█ █   █ █▄   ▄██▀▀█ █   █ █
█ █▄▄▄█ █ ▀█▀█▄▀ ██ █▄▄▄█ █
█▄▄▄▄▄▄▄█ ▀▄█ █▄▀ █▄▄▄▄▄▄▄█
█▄▄▀ █▀▄▄██ ▀▄ ▀██▀  ▄▀▄▄▀█
█ ▄▄▀▄▀▄▀▄▄▄█▄  ▀▄▄▀ ▀▀█▄▄█
█▄▄ ▄ █▄▄██▀██▄ █▀█ ▄█ ██▀█
█▄▀██▀█▄ █▄█ ▄▀██ ▄▄ ▀▀██▄█
█▄▄██▄▄▄▄  ▀▀██▄  ▄▄▄ █ ▄ █
█ ▄▄▄▄▄ █▄▄▀▀▄▄▄  █▄█  ▀▄ █
█ █   █ █▀▀ ▄█ ▀▀▄ ▄▄ █▀▄██
█ █▄▄▄█ █▀▄ ██ ▄█  █▄  ▄█▄█
█▄▄▄▄▄▄▄█▄██▄█▄▄█▄███▄▄█▄▄█

› Metro waiting on exp://192.168.104.200:19000
› Scan the QR code above with Expo Go (Android) or the Camera app (iOS)

› Press a │ open Android
› Press i │ open iOS simulator
› Press w │ open web

› Press r │ reload app
› Press m │ toggle menu

› Press ? │ show all commands

Logs for your project will appear below. Press Ctrl+C to exit.
Started Metro Bundler
Enter fullscreen mode Exit fullscreen mode

Now launch the Expo Go app on your mobile, and scan the QR code. You should have something like this:

Awesome, you can go ahead to install

npm i react-native-web react-dom
Enter fullscreen mode Exit fullscreen mode

This will enable viewing the app on your web browser.

Conclusion

Amplify DataStore allows you to start persisting data locally to your device with DataStore, even without an AWS account. In this article, we learned about Expo SQLite and we went on to build a React native app with Expo SQLite adapter and AWS Amplify DataStore.

Top comments (3)

Collapse
 
zolipeto profile image
PEZO - Zoltán Pető

Thanks for the great article!

A couple of questions regarding offline-first apps:

  1. Let’s say instead fetchData, the user has its own data (in given Models). How can we migrate the data models with aws amplify+expo the best way?

2.a. In case of observation of collections/items: can we subscribe to add/update/delete explicitly? If so: can we setup nested subscriptions? (Eg. setup “onCollectionAdd(collection, newItem)” which when fires sets up “onItemUpdate(item, entryChanges)”, so we could get notified about item entry changes on collection level too.

2.b. If previous is possible: isn’t that too slow regarding expo-sqlite based implementation and other GraphQL related high level implementation concerns?

Collapse
 
gurpreet31cpu profile image
Gurpreet31-cpu

Thanks for great tutorial!

React Native window (UWP) app is crashing If I use AWS Datastore. Does AWS datastore support 'react native window desktop' app?

React Native window (UWP) app is crashing If I use AWS Datastore by importing like below:
import {Datastore} from '@aws-amplify/datastore'
or
import { DataStore } from 'aws-amplify';

But its working fine in Android/iOS devices.

Collapse
 
abdallahshaban profile image
Abdallah Shaban

Hi @gurpreet31cpu - can you please create a Github issue on this repo here for the Amplify team to investigate this? https://github.com/aws-amplify/amplify-js/issues/new?assignees=&labels=&template=1.bug_report.yaml