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
- AWS Amplify configured
amplify configure
- Expo CLI installed
npm install -g expo-cli
- 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
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
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
-
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
}
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
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
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
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'
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
});
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>
)
}
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)
}
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)
}
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>
))
}
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 })
}
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()
})
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
Awesome!
Now let’s go ahead and run the app. Run this command on your terminal.
expo start
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
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
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)
Thanks for the great article!
A couple of questions regarding offline-first apps:
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?
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.
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