AWS AppSync
AWS AppSync Developer Guide

Building a React Native Client App

AWS AppSync integrates with the Apollo GraphQL client for building client applications. AWS provides Apollo plugins for offline support, authorization, and subscription handshaking. You can use the Apollo client directly, or you can use it with some of the client helpers provided in the AWS AppSync SDK.

This tutorial shows you how to use AWS AppSync with React Apollo, which uses ReactJS constructs and patterns with GraphQL. For the latest AWS AppSync SDK documentation, including API definitions and sample apps, see aws-mobile-appsync-sdk-js on GitHub.

Alternatively, for a simple, lightweight GraphQL client that is integrated into the AWS ecosystem for authentication, object storage, analytics, and other features, see AWS Amplify and the GraphQL client.

Before You Begin

This tutorial is set up for a sample API using the schema from the DynamoDB resolvers tutorial. To follow along with the complete flow, you can optionally walk through that tutorial first. If you want to do more customization of GraphQL resolvers, such as those that use DynamoDB, see the Resolver Mapping Template Reference. The application uses the following starting schema:

schema { query: Query mutation: Mutation } type Mutation { addPost(id: ID! author: String! title: String content: String! url: String!): Post! updatePost(id: ID! author: String! title: String content: String url: String expectedVersion: Int!): Post! deletePost(id: ID!, expectedVersion: Int): Post } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int version: Int! } type PaginatedPosts { posts: [Post!]! nextToken: String } type Query { allPost(count: Int, nextToken: String): PaginatedPosts! getPost(id: ID!): Post }

This schema defines a Post type and operations to add, get, update, and delete Post objects.

Get the GraphQL API Endpoint

After you create your GraphQL API, you'll need to get the API endpoint (URL) so you can use it in your client application. You can get the API endpoint in either of the following ways:

  • In the AWS AppSync console, choose Home, and then choose GraphQL URL to see the API endpoint.

  • Run the following CLI command:

aws appsync get-graphql-api --api-id $GRAPHQL_API_ID

Download a Client Application

To show you how to use AWS AppSync, we first review a React Native application (bootstrapped with create-react-native-app) with just a local array of data. Then we add AWS AppSync capabilities to it. To begin, download a sample application where we can add, update, and delete posts.

Understanding the React Native Sample App

The React Native sample app has three major files:

  • ./src/App.js: The main entry point of the application. Renders the main application shell with two components named AddPost and AllPosts, and has a local array of data named posts that is passed as a prop to the other components.

  • ./src/Components/AddPost: A React Native component that contains a form that enables a user to enter new information about a post, such as the author and title.

  • ./src/Components/AllPosts: A React Native component that lists all existing posts from the posts array that App.js created. It enables you to edit or delete existing posts.

Run your app as follows, and test it to verify that it works:

yarn && yarn start

Import the AWS AppSync SDK into Your App

In this section, you'll add AWS AppSync to your existing React Native app. Add the following dependencies to your application:

yarn add react-apollo graphql-tag aws-sdk

Next, add in the AWS AppSync SDK, including the React extensions:

yarn add aws-appsync yarn add aws-appsync-react

From the AWS AppSync console, go to your GraphQL API integration page at the root of the navigation bar on the left ). At the bottom of the page, choose JavaScript. Next, click Download Config and save the aws-exports.js configuration file into ./src.

To interact with AWS AppSync, your client needs to define GraphQL queries and mutations. This is commonly done in separate files, as follows:

mkdir ./Queries touch ./Queries/AllPostsQuery.js touch ./Queries/DeletePostMutation.js touch ./Queries/NewPostMutation.js touch ./Queries/UpdatePostMutation.js

Edit and save AllPostsQuery.js:

import gql from 'graphql-tag'; export default gql` query AllPosts { allPost { posts { __typename id title author version } } }`;

Edit and save DeletePostMutation.js:

import gql from 'graphql-tag'; export default gql` mutation DeletePostMutation($id: ID!, $expectedVersion: Int!) { deletePost(id: $id, expectedVersion: $expectedVersion) { __typename id author title version } }`;

Edit and save NewPostMutation.js:

import gql from 'graphql-tag'; export default gql` mutation AddPostMutation($id: ID!, $author: String!, $title: String!) { addPost( id: $id author: $author title: $title content: " " url: " " ) { __typename id author title version } }`;

Edit and save UpdatePostMutation.js:

import gql from 'graphql-tag'; export default gql` mutation UpdatePostMutation($id: ID!, $author: String, $title: String, $expectedVersion: Int!) { updatePost( id: $id author: $author title: $title expectedVersion: $expectedVersion ) { __typename id author title version } }`;

Edit your App.js file, as follows:

import AWSAppSyncClient from "aws-appsync"; import { Rehydrated } from 'aws-appsync-react'; import { AUTH_TYPE } from "aws-appsync/lib/link/auth-link"; import { graphql, ApolloProvider, compose } from 'react-apollo'; import * as AWS from 'aws-sdk'; import AppSync from './aws-exports.js'; import AllPostsQuery from './Queries/AllPostsQuery'; import NewPostMutation from './Queries/NewPostMutation'; import DeletePostMutation from './Queries/DeletePostMutation'; import UpdatePostMutation from './Queries/UpdatePostMutation';

After all the import statements, add the following code:

const client = new AWSAppSyncClient({ url: AppSync.graphqlEndpoint, region: AppSync.region, auth: { type: AUTH_TYPE.API_KEY, apiKey: AppSync.apiKey, //type: AUTH_TYPE.AWS_IAM, //Note - Testing purposes only /*credentials: new AWS.Credentials({ accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY })*/ //IAM Cognito Identity using AWS Amplify //credentials: () => Auth.currentCredentials(), //Cognito User Pools using AWS Amplify // type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, // jwtToken: async () => (await Auth.currentSession()).getIdToken().getJwtToken(), }, });

You can switch the AUTH_TYPE value to use API keys, IAM (including short-term credentials from Amazon Cognito federated identities), or Amazon Cognito user pools. We recommend you use either IAM or Amazon Cognito user pools after onboarding with an API key. The previous code shows how to use the default configuration of AWS AppSync with an API key, referencing the aws-exports.js file you downloaded. When you're ready to add other authorization methods to your application, you can use the AWS Amplify library to quickly add these capabilities to your application. The corresponding AWS Amplify methods for the AWS AppSync client constructor are included above, and an import of the library with configuration would look similar to the following:

import Amplify, { Auth } from 'aws-amplify'; import { withAuthenticator } from 'aws-amplify-react'; Amplify.configure(awsmobile); //...code const AppWithAuth = withAuthenticator(App, true);

For more information about using AWS Amplify, see the library documentation.

Replace the App component entirely, so that it looks like the following:

class App extends Component { state = { posts: [] }; render() { return ( <View style={styles.container}> <AddPostWithData /> <AllPostsWithData /> </View> ); } }

Delete the posts variable in your code because the app state comes from AWS AppSync. Also, change the initial state of the App component to the following:

state = { posts: [] };

At the bottom of your App.js file, define the following higher-order component (HOC):

const AllPostsWithData = compose( graphql(AllPostsQuery, { options: { fetchPolicy: 'cache-and-network' }, props: (props) => ({ posts: props.data.allPost && props.data.allPost.posts, }) }), graphql(DeletePostMutation, { props: (props) => ({ onDelete: (post) => props.mutate({ variables: { id: post.id, expectedVersion: post.version }, optimisticResponse: () => ({ deletePost: { ...post, __typename: 'Post' } }), }) }), options: { refetchQueries: [{ query: AllPostsQuery }], update: (proxy, { data: { deletePost: { id } } }) => { const query = AllPostsQuery; const data = proxy.readQuery({ query }); data.allPost.posts = data.allPost.posts.filter(post => post.id !== id); proxy.writeQuery({ query, data }); } } }), graphql(UpdatePostMutation, { props: (props) => ({ onEdit: (post) => { props.mutate({ variables: { ...post, expectedVersion: post.version }, optimisticResponse: () => ({ updatePost: { ...post, __typename: 'Post', version: post.version + 1 } }), }) } }), options: { refetchQueries: [{ query: AllPostsQuery }], update: (dataProxy, { data: { updatePost } }) => { const query = AllPostsQuery; const data = dataProxy.readQuery({ query }); data.allPost.posts = data.allPost.posts.map(post => post.id !== updatePost.id ? post : { ...updatePost }); dataProxy.writeQuery({ query, data }); } } }) )(AllPosts); const AddPostWithData = graphql(NewPostMutation, { props: (props) => ({ onAdd: post => props.mutate({ variables: post, optimisticResponse: () => ({ addPost: { ...post, __typename: 'Post', version: 1 } }), }) }), options: { refetchQueries: [{ query: AllPostsQuery }], update: (dataProxy, { data: { addPost } }) => { const query = AllPostsQuery; const data = dataProxy.readQuery({ query }); data.allPost.posts.push(addPost); dataProxy.writeQuery({ query, data }); } } })(AddPost);

Finally, replace export default App with the ApolloProvider as follows:

const WithProvider = () => ( <ApolloProvider client={client}> <Rehydrated> <App /> </Rehydrated> </ApolloProvider> ); export default WithProvider;

Test Your Application

yarn start

Offline Settings

There are important considerations that you need to account for if you want an optimistic UI for an application, where data can be manipulated when the device is in an offline state. Many of these settings are documented in the official Apollo documentation, however, we call out several of them here that you should configure.

First, the AWS AppSync client enables you to disable offline capabilities if you simply want to use GraphQL in an always-online scenario. To do this, you pass an additional option when instantiating your client, named disableOffline, as follows:

const client = new AWSAppSyncClient({ url: AppSync.graphqlEndpoint, region: AppSync.region, auth: { type: AUTH_TYPE.API_KEY, apiKey: AppSync.apiKey, }, disableOffline: true });
  • fetchPolicy: This option enables you to specify how a query interacts with the network versus local in-memory caching. AWS AppSync persists this cache to a platform-specific storage medium. If you are using the AWS AppSync client in offline scenarios (disableOffline:false), you MUST set this value to cache-and-network:

    options: { fetchPolicy: 'cache-and-network' }
  • optimisticResponse: This option enables you to pass a function or an object to a mutation for updating your UI before the server responds with the result. This is needed in offline scenarios (and for slower networks) to ensure that the UI is updated when the device has no connectivity. Optionally, you can use this if you have set disableOffline:true. For example, if you were adding a new object to a list, you might use the following:

    onAdd: post => props.mutate({ variables: post, optimisticResponse: () => ({ addPost: { __typename: 'Post', ups: 1, downs: 1, content: '', url: '', version: 1, ...post } }), })

Typically, you use optimisticResponse in conjunction with the update option for React Apollo's component, which can trigger during an offline mutation. If you want the UI to update offline for a specific query, you need to specify that query as part of the readQuery and writeQuery options on the cache, as shown following:

options: { refetchQueries: [{ query: AllPostsQuery }], update: (dataProxy, { data: { addPost } }) => { const query = AllPostsQuery; const data = dataProxy.readQuery({ query }); data.allPost.posts.push(addPost); dataProxy.writeQuery({ query, data }); } }

When this happens, the AWS AppSync persistent store is automatically updated in response to the Apollo cache update. Upon network reconnection, it will synchronize with your GraphQL endpoint. You could also modify more than one query when offline, in which case you could run the process multiple times in the same update block.

Make Your Application Real Time

Edit your schema with the subscription type, as follows:

schema { query: Query mutation: Mutation subscription: Subscription } type Mutation { addPost(id: ID! author: String! title: String content: String! url: String!): Post! updatePost(id: ID! author: String! title: String content: String url: String expectedVersion: Int!): Post! deletePost(id: ID!, expectedVersion: Int): Post } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int version: Int! } type PaginatedPosts { posts: [Post!]! nextToken: String } type Query { allPost(count: Int, nextToken: String): PaginatedPosts! getPost(id: ID!): Post } type Subscription { newPost: Post @aws_subscribe(mutations:["addPost"]) }

Notice that the @aws_subscribe specifies which mutations trigger a subscription. You can add more mutations in this array to meet your application needs.

The subscription type newPost needs to be passed into an option named updateQuery of the React Apollo client to update your UI dynamically when a subscription is received. Ensure that this field name matches the subscription type in the following example code.

In your App.js file, edit the AllPostsWithData HOC to include subscribeToNewPost in the props field, as follows:

const AllPostsWithData = compose( graphql(AllPostsQuery, { options: { fetchPolicy: 'cache-and-network' }, props: (props) => ({ posts: props.data.allPost && props.data.allPost.posts, // START - NEW PROP : subscribeToNewPosts: params => { props.data.subscribeToMore({ document: NewPostsSubscription, updateQuery: (prev, { subscriptionData: { data : { newPost } } }) => ({ ...prev, allPost: { posts: [newPost, ...prev.allPost.posts.filter(post => post.id !== newPost.id)], __typename: 'PaginatedPosts' } }) }); }, // END - NEW PROP } }) }), ...//more code
touch ./Queries/NewPostsSubscription.js
import gql from 'graphql-tag'; export default gql` subscription NewPostSub { newPost { __typename id title author version } }`;

Add the following import statement at the top of your App.js file as follows:

import NewPostsSubscription from './Queries/NewPostsSubscription';

Modify the defaultProps in the AllPosts.js component, as follows:

static defaultProps = { posts: [], onDelete: () => null, onEdit: () => null, subscribeToNewPosts: () => null, }

Add the following lifecycle method to your AllPosts component in AllPosts.js:

componentWillMount(){ this.props.subscribeToNewPosts(); }

Now try running your app again by typing yarn start. Add a new post via the console, with a mutation on addPost. You should see real-time data appear in your client application.

Conflict Resolution

When clients make a mutation, either online or offline, they can send a version number with the payload (named expectedVersion) for AWS AppSync to check before writing to Amazon DynamoDB. A DynamoDB resolver mapping template can be configured to perform conflict resolution in the cloud, which you can learn about in Resolver Mapping Template Reference for DynamoDB. If the service determines it needs to reject the mutation, data is sent to the client and you can optionally run an additional callback to perform client-side conflict resolution.

For example, suppose you had a mutation with DynamoDB set for checking the version, and the client sent expectedVersion:0, as in the following example:

graphql(UpdatePostMutation, { props: (props) => ({ onEdit: (post) => { props.mutate({ variables: { ...post, expectedVersion: 0 }, optimisticResponse: () => ({ updatePost: { ...post, __typename: 'Post', version: post.version + 1 } }), }) } }),...more code

This would fail the version check because 0 would be lower than any of the current values. You can then define a custom callback conflict resolver. A custom conflict resolver receives the following variables:

  • mutation: GraphQL statement of a mutation

  • mutationName: Optional if a name of a mutation is set on a GraphQL statement

  • variables: Input parameters of the mutation

  • data: Response from AWS AppSync of actual data in DynamoDB

  • retries: Number of times a mutation has been retried

For example, you could have the following custom callback conflict resolver as follows:

const conflictResolver = ({ mutation, mutationName, variables, data, retries }) => { switch (mutationName) { case 'UpdatePostMutation': return { ...variables, expectedVersion: data.version, }; default: return false; } }

In the previous example, you can do a logical check on the mutationName and then rerun the mutation with the correct version that AWS AppSync returned.

Note: We recommend doing this only in rare cases. Usually, we recommend that you let the AWS AppSync service define conflict resolution to prevent race conditions from occurring. If you don't want to retry, simply return DISCARD.

Now, to use this callback, pass it into the AWS AppSync client instantiation as follows:

const client = new AWSAppSyncClient({ url: awsconfig.ENDPOINT, region: awsconfig.REGION, auth: authInfo, conflictResolver, });