To view and deploy the app covered in this chapter, check out this repo.
So far in this book we've worked with both REST APIs and GraphQL APIs. When working with the GraphQL APIs so far, we've used the API class to directly call mutations and queries against the API.
Amplify also supports another type of API for interacting with AppSync, Amplify DataStore. DataStore has a different approach than a traditional GraphQL API.
Instead of interacting with the GraphQL API itself using queries and mutations, DataStore introduces a client-side SDK that persists the data locally using the local storage engine of the platform you are working with (i.e. IndexDB for web, SQLLite for native iOS and Android). DataStore then automatically syncs the local data to the GraphQL backend for you as updates are made both locally and remotely.
Using the DataStore SDK, you then only have to perform the operations like save, update, and delete, writing directly to DataStore itself. DataStore handles everything else for you, syncing your data to the cloud when you have an internet connection, and when not connected to the internet, will queue it for the next time you are connected to the internet.
DataStore also handles conflict detection and resolution for you with one of built-in 3 conflict resolution strategies:
- AutoMerge - GraphQL type information on an object is inspected at runtime to perform merge operations
- Optimistic Concurrency - the latest written item to your database will be used with a version check against the incoming record
- Custom - Use a Lambda function and apply any custom business logic you wish to the process when merging or rejecting updates
Amplify DataStore is a combination of a few things:
- AppSync GraphQL API
- A local storage repository and syncing engine that also persists data offline
- Client-side SDK for interacting with the local storage repository
- Special sync-enabled GraphQL resolvers (generated by the Amplify CLI) that enable sophisticated conflict detection and conflict resolution on the server
When getting started with DataStore, you still create the API as we have done in past chapters. The main difference is, when creating the API, you will enable conflict detection in the advanced settings of the CLI flow.
From there, to enable DataStore on the client, we need to create models for DataStore to use to interact with the storage repository. This can easily be done by just using the GraphQL schema you already have and running amplify codegen models
from the CLI.
Now, you are all set up and can begin interacting with DataStore.
To interact with the Store, you first import the DataStore
API from Amplify and the Model you'd like to use. From there, you can perform actions against the store.
Let's have a look at the different operations that are available.
Importing the model and DataStore API
import { DataStore } from '@aws-amplify/datastore'
import { Message} from './models'
Saving data with Amplify DataStore
await DataStore.save(
new Message({
title: 'Hello World',
sender: 'Chris'
})
))
Reading data with Amplify DataStore
const posts = await DataStore.query(Post)
Deleting data with Amplify DataStore
const message = await DataStore.query(Message, '1234567')
DataStore.delete(message)
Updating data with Amplify DataStore
const message = await DataStore.query(Message, '1234567')
await DataStore.save(
Post.copyOf(message, updated => {
updated.title = 'My new title'
})
)
Observing / subscribing to changes in data for real-time functionality
const subscription = DataStore.observe(Message).subscribe(msg => {
console.log(message.model, message.opType, message.element)
});
You can apply predicate filters against the DataStore using the fields defined on your GraphQL type along with the following conditions supported by DynamoDB:
Strings: eq | ne | le | lt | ge | gt | contains | notContains | beginsWith | between
Numbers: eq | ne | le | lt | ge | gt | between
Lists: contains | notContains
For example if you wanted a list of all messages that have a title that includes "Hello":
const messages = await DataStore.query(Message, m => m.title('contains', 'Hello'))
You can also chain multiple predicates into a single operation:
const message = await DataStore
.query(Message, m => m.title('contains', 'Hello').sender('eq', 'Chris'))
Using predicates enables you to have many ways to retrieve different selection sets from your local data. Instead of retrieving the entire collection and filtering on the client, you are able to query from the store exactly the data that you need.
The app that we will build is a real-time and offline-first message board.
Users of the app can create a new message and all other users will receive the message in real-time.
If a user goes offline, they will continue to be able to create messages and once they are online the messages will be synced with the back end, and all other messages created by other users will also be fetched and synced locally.
Our app will perform three types of operations against the DataStore API:
save - Creating a new item in the DataStore, performs a GraphQL mutation behind the scenes.
query - Reading from the DataStore, returns a list (array), performs a GraphQL query behind the scenes
observe - Listening for changes (create, update, delete) in data, performs a GraphQL subscription behind the scenes.
Let's get started!
To get started, we will create a new React project, initialize an Amplify app, and install the dependencies.
The first thing we will do is create the React project:
$ npx create-react-app rtmessageboard
$ cd rtmessageboard
Next, we will install the local dependencies.
Amplify supports both a full installation of Amplify, or scoped (modular) installations for specific APIs. Scoped packages reduce the bundle size since we are only installing the code that we are using, and are something we have not yet used in this book. Since we are only using the DataStore API, we can install the scoped DataStore package.
We will also install Ant Design (antd
) for styling, React Color (react-color
) for a nice and easy to use color picker, and the scoped dependency for Amplify Core in order to still configure the Amplify app with our aws-exports.js configuration.
$ npm install @aws-amplify/core @aws-amplify/datastore antd react-color
Now, initialize a new Amplify project:
$ amplify init
# Follow the steps to give the project a name, environment a name, and set the default text editor.
# Accept defaults for everything else and choose your AWS Profile.
Next, we will create the AppSync GraphQL API:
$ amplify add API
? Please select from one of the below mentioned services: GraphQL
? Provide API name: rtmessageboard
? Choose the default authorization type for the API: API key
? Enter a description for the API key: public
? After how many days from now the API key should expire (1-365): 7 (or your preferred expiration)
? Do you want to configure advanced settings for the GraphQL API: Yes
? Configure additional auth types: N
? Configure conflict detection: Y
? Select the default resolution strategy: Auto Merge
? Do you have an annotated GraphQL schema: N
? Do you want a guided schema creation: Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now: Y
Update the schema with the following type:
type Message @model {
id: ID!
title: String!
color: String
image: String
createdAt: String
}
Now that we have created the GraphQL API and we have a GraphQL Schema to work with, we can create the Models we'll need for working the the local DataStore API (based on the GraphQL Schema):
$ amplify codegen models
This will create a new folder in our project called models
. Using the models in this folder, we can start interacting with the DataStore API.
Next, we can deploy the API:
$ amplify push --y
Now that the back end has been deployed, we can start writing the client-side code.
First, open src/index.js and configure the Amplify app by adding the following code below the last import:
import Amplify from '@aws-amplify/core'
import config from './aws-exports'
Amplify.configure(config)
Notice that we are now importing from @aws-amplify/core
instead of aws-amplify
.
Next open App.js and update it with the following code:
import React, { useState, useEffect } from 'react'
import { SketchPicker } from 'react-color'
import { Input, Button } from 'antd'
import { DataStore } from '@aws-amplify/datastore'
import { Message} from './models'
const initialState = { color: '#000000', title: '', }
function App() {
const [formState, updateFormState] = useState(initialState)
const [messages, updateMessages] = useState([])
const [showPicker, updateShowPicker] = useState(false)
useEffect(() => {
fetchMessages()
const subscription = DataStore.observe(Message).subscribe(() => fetchMessages())
return () => subscription.unsubscribe()
}, [])
async function fetchMessages() {
const messages = await DataStore.query(Message)
updateMessages(messages)
}
function onChange(e) {
if (e.hex) {
updateFormState({ ...formState, color: e.hex})
} else { updateFormState({ ...formState, [e.target.name]: e.target.value}) }
}
async function createMessage() {
if (!formState.title) return
await DataStore.save(new Message({ ...formState }))
updateFormState(initialState)
}
return (
<div style={container}>
<h1 style={heading}>Real Time Message Board</h1>
<Input
onChange={onChange}
name="title"
placeholder="Message title"
value={formState.title}
style={input}
/>
<div>
<Button onClick={() => updateShowPicker(!showPicker)}style={button}>Toggle Color Picker</Button>
<p>Color: <span style={{fontWeight: 'bold', color: formState.color}}>{formState.color}</span></p>
</div>
{
showPicker && <SketchPicker color={formState.color} onChange={onChange} />
}
<Button type="primary" onClick={createMessage}>Create Message</Button>
{
messages.map(message => (
<div key={message.id} style={{...messageStyle, backgroundColor: message.color}}>
<div style={messageBg}>
<p style={messageTitle}>{message.title}</p>
</div>
</div>
))
}
</div>
);
}
const container = { width: '100%', padding: 40, maxWidth: 900 }
const input = { marginBottom: 10 }
const button = { marginBottom: 10 }
const heading = { fontWeight: 'normal', fontSize: 40 }
const messageBg = { backgroundColor: 'white' }
const messageStyle = { padding: '20px', marginTop: 7, borderRadius: 4 }
const messageTitle = { margin: 0, padding: 9, fontSize: 20 }
export default App;
Let's walk through the most important parts of what's going on in this component.
- First, we import the
DataStore
API from Amplify as well as theMessage
model. - We create three pieces of component state using the
useState
hook:formState
- This object manages the state for the form, including the messagetitle
and messagecolor
that will be used to display the background color of the messagemessages
- This will manage the array of messages once they are fetched from DataStoreshowPicker
- This will manage a boolean value that will be toggled to show and hide a color picker to fill thecolor
value for the message (by default, is set to black)
- When the component loads, we fetch all messages by invoking the
fetchMessages
function and create a subscription to listen to message updates. When a subscription is fired, we also invoke thefetchMessages
function because we know there has been an update and we would like to update the app with the most recent data coming back from the API. - The
fetchMessages
function callsDataStore.query
and then updates the component state with the returned array of messages - The
onChange
handler handles the updates to both the form input as well as the color picker being changed - In
createMessage
, we first check to makes sure the title field is populated. If it is, we save the message usingDataStore.save
and then reset the form state.
Next, let's test it out:
$ npm start
Now, try going offline, creating a new mutation and then coming back online. You should notice that, when back online, the app takes all of the messages created when you were offline and creates them in the database.
To verify this, open the AppSync API in the AWS Console:
$ amplify console api
? Please select from one of the below mentioned services: GraphQL
Next, click on Data Sources and then open the Message Table resource. You should now see the items in the Message Table.
To test out the real-time functionality, open a another window so that you have two windows running the same app.
Create a new item in one window, and see the update come through automatically in the other window.
-
Amplify enables two different APIs to interact with AppSync, the
API
category as well asDataStore
-
When using DataStore, you are no longer sending HTTP requests directly to the API. Instead, you are writing to the local storage engine and DataStore then takes care of the syncing to and from the cloud.
-
Amplify DataStore works offline by default.
https://github.com/dabit3/full-stack-serverless-code/tree/master/rtmessageboard
@cnegrisanu
I'm facing the exact same issue.
Did you find out a good solution?