In these tutorials, I'm showing you how to create and interact with a GraphQL database using AWS AppSync and React Native. This app will have real-time and offline functionality, something we get out of the box with AppSync.
In the previous post, we set up our GraphQL back-end with the Amazon AppSync service. Check it out if you haven't already. Or, if you want an introduction to GraphQL, take a look at some of our other posts.
- GraphQLCode an App With GraphQL, React Native, and AWS AppSync: The Back-End
- JavaScriptWhat Is GraphQL?
In this post, we'll wrap it all up by walking through the build of the React Native client. The project is a bit too complicated to walk you through step by step, but I'll explain the project architecture and show you the key parts of the source code.
Overview of the App Architecture and Folder Structure
Our application will have a main entry point that will consist of two tabbed views. One tab will list the cities from our GraphQL database, and the other will be the input form to add a new city. The Cities tab will be a navigator that will allow the user to navigate to the individual cities.
We will store the main components in the source folder, and will have other folders in the src directory to hold our GraphQL mutations, queries, and subscriptions.
We will also have an assets folder to hold our images.
Creating and Configuring the React Native Client
For reference, take a look at the final code for this app in the tutorial GitHub repo, but I'll outline some of the steps I took to create the app from scratch.
First, we created a new React Native application using Expo.
Once in the newly created project, we installed our dependencies. For the GraphQL and AppSync functionality, we used the following dependencies:
aws-appsync aws-appsync-react graphql-tag react-apollo uuid
We also used the following dependencies for the UI design:
react-navigation react-native-elements react-native-vector-icons
Also, once the Vector Icons library was installed, we linked it:
react-native link react-native vector-icons
After installing our dependencies, we downloaded the AppSync.js file from our AppSync console. In our AppSync project console, we chose React Native at the bottom, and clicked on the orange Download button to download this config file.
This config file holds the AppSync client information we needed to create a new client.
Configuring the Provider and Store
The top level of the app is where we will do our configuration to wire up the AppSync API with the React Native client. If you have used Redux or React Apollo before, this will all be familiar. If you have not, just remember that any child of a Provider
, in our case the ApolloProvider
, will have access to its given functionality.
The following code is our new App.js file, which is the main component imported from our index.js entrypoint.
import React from 'react' import Tabs from './src/Tabs' import AWSAppSyncClient from "aws-appsync"; import { Rehydrated } from 'aws-appsync-react'; import { ApolloProvider } from 'react-apollo'; import appSyncConfig from './aws-exports'; const client = new AWSAppSyncClient({ url: appSyncConfig.graphqlEndpoint, region: appSyncConfig.region, auth: { type: appSyncConfig.authType, apiKey: appSyncConfig.apiKey, } }); const WithProvider = () => (<ApolloProvider client={client}><Rehydrated><Tabs /></Rehydrated></ApolloProvider> ); export default WithProvider
In this file, we are setting up a new AppSync client using a combination of the AWSAppSyncClient
constructor from aws-appsync
as well as the configuration in our aws-exports.js file, which provides the GraphQL API URL, region, authentication type, and authentication API key.
We then wrap our main entrypoint, the Tabs.js file which will hold our tab navigation, in an ApolloProvider
and pass in the AppSync client as the client prop. We also wrap the Tabs
component in a Rehydrated
component that we import from aws-appsync-react
. This will make sure that we have read from async storage and have rehydrated our cache before rendering the UI.
Now our app will be able to query data from our AppSync endpoint, and also to perform mutations and subscriptions!
Navigation
The main entry point of the app is a tabbed navigation, implemented in the Tabs.js file with React Navigation.
What we've done here is create and export a TabNavigator
with two tabs. These are:
- Cities: This component lists our cities, and it is a navigator component in and of itself. This component is a navigator because we want to be able to navigate to each individual city and view the locations within the city.
- AddCity: This component is a form for us to be able to add new cities.
Reusable Components
This app has only one reusable component, a customized TextInput
. Since we will be duplicating this style and functionality over and over, we decided to make it its own component. The input component is implemented in Input.js.
Cities List and City Navigation
The main view of the app is a list of cities that we will be retrieving from GraphQL. We want to be able to navigate from each listed city to a detail view of that city where we can add locations.
To do this, we make Cities.js its own StackNavigator, and City.js the component we navigate to when choosing a city. When clicking on a city in Cities
, we pass its name and id as props to City
.
Cities.js
In this component, we are fetching using the listCities
query, and we are also subscribing to the NewCitySubscription
, so that when a new city is added, even from another client, we'll handle that subscription and update our array of cities. The listCities
query makes an array of cities available in our component as this.props.cities
.
City.js
In this component, we are passed a city as props from navigation (available as props.navigation.state.params.city
). We use the city id
value to fetch the list of locations for the chosen city using the listLocations
query. We subscribe to new locations in a similar way to how we subscribed to new cities in Cities.js, using the NewLocationSubscription
subscription. We also provide optimisticResponse
and update
functions for when a new city is added.
Adding Cities
Finally, we need to implement the functionality for adding new cities to our GraphQL API in the AddCity.js file. To do this, we have wired up a mutation along with a form that will call createCity
, passing the value of the form input.
AddCity has an onAdd
function that we define in our GraphQL composition, which not only writes a new city to our GraphQL database, but also implements an optimistic UI using a combination of optimisticResponse
and update
.
Mutations, Queries, and Subscriptions
Mutations, queries, and subscriptions are the core functionality for integrating with our GraphQL API. In our app, this functionality is implemented in the Cities.js, City.js, and AddCity.js files using the AppSync client.
Let's take a closer look at how mutations, queries, and subscriptions are implemented in our code.
Queries
First, let's look at how to create and export a GraphQL query that could interact with the listCities query in our AppSync Schema. This code is contained in the src/queries/ListCities.js file.
import gql from 'graphql-tag'; export default gql` query listCities { listCities { items { name country id } } }`
Next, we import this query in the Cities.js file, along with some helpers from react-apollo
, and wire up the component that we would like to have access to this data using compose
and graphql
from react-apollo
.
import { compose, graphql } from 'react-apollo' import ListCities from './queries/ListCities' class Cities extends React.Component { // class definition here // now have access to this.props.cities } export default compose( graphql(ListCities, { props: props => ({ cities: props.data.listCities ? props.data.listCities.items : [], }) }) )(CityList)
Now we have access to the cities array from our GraphQL server as a prop. We can use this.props.cities
to map over the cities array coming in from GraphQL.
Mutations
To create a mutation, first we need to create a basic GraphQL mutation and export it. We do this in the src/mutations/CreateCity.js file.
import gql from 'graphql-tag' export default gql` mutation addCity($name: String!, $country: String!, $id: ID!) { createCity(input: { name: $name, country: $country, id: $id }) { name country id } } `
Now we can import this mutation (along with the Apollo helpers) in the AddCity.js file and use it in a component:
import { compose, graphql } from 'react-apollo' import AddCityMutation from './mutations/AddCity' class AddCity extends React.Component { // class definition here // now have access to this.props.onAdd() } export default compose( graphql(AddCityMutation, { props: props => ({ onAdd: city => props.mutate({ variables: city }) }) }) )(AddCity)
Now, we have access to a prop called onAdd
, which we pass an object we would like to send to the mutation!
Subscriptions
Subscriptions allow us to subscribe to data changes and have them update in our application in real time. If we were to change our database by adding or removing a city, we would like our app to update in real time.
First, we need to create the mutation and export it so we can have access to it within the client. We save this in the src/subscriptionsNewCitySubscriptions.js file.
import gql from 'graphql-tag' export default gql` subscription NewCitySub { onCreateCity { name country id } }`;
Now we can import and attach the subscription in Cities.js. We already looked at how to get the cities from our API. Let's now update this functionality to subscribe to new changes and update the cities array when a new city is added.
import AllCity from './queries/AllCity' import NewCitiesSubscription from './subscriptions/NewCitySubscription'; import { compose, graphql } from 'react-apollo' class Cities extends React.Component { componentWillMount(){ this.props.subscribeToNewCities(); } render() { // rest of component here } } export default compose( graphql(ListCities, { options: { fetchPolicy: 'cache-and-network' }, props: (props) => { return { cities: props.data.listCities ? props.data.listCities.items : [], subscribeToNewCities: params => { props.data.subscribeToMore({ document: NewCitiesSubscription, updateQuery: (prev, { subscriptionData: { data : { onCreateCity } } }) => { return { ...prev, listCities: { __typename: 'CityConnection', items: [onCreateCity, ...prev.listCities.items.filter(city => city.id !== onCreateCity.id)] } } } }) } } } }) )(Cities)
We add a new prop called subscribeToNewCities
, which we call in componentDidMount
. In the subscription, we pass in a document (the subscription definition) and updateQuery
to describe what we want to happen when this updates.
We destructure createCity
(containing the mutation) from the props that are passed into the updateQuery
function, and return all existing values along with an updated listCities
array containing the previous cities along with the new city data that we get from createCity
.
Optimistic UI
What if we don't want to wait for the subscription to return the most up-to-date data from our API in order to update our UI?
If a user creates a new city, we want to automatically add it the cities array and have it render in our app before receiving confirmation from the back-end service.
We can do that easily using a few techniques and functions.
Let's update our AddCityMutation
to the following (you can view this code in the AddCity.js source file):
import { compose, graphql } from 'react-apollo' import AddCityMutation from './mutations/AddCity' class AddCity extends React.Component { // class definition here // now have access to this.props.onAdd() } export default compose( graphql(AddCityMutation, { props: props => ({ onAdd: city => props.mutate({ variables: city, optimisticResponse: { __typename: 'Mutation', createCity: { ...city, __typename: 'City' } }, update: (proxy, { data: { createCity } }) => { const data = proxy.readQuery({ query: ListCities }); data.listCities.items.unshift(createCity); proxy.writeQuery({ query: ListCities, data }); } }) }) }) )(AddCity)
Here, we've added two new properties to the mutate function argument object:
optimisticResponse
defines the new response you would like to have available in the update function.update
takes two arguments, the proxy (which allows you to read from the cache) and the data you would like to use to make the update. We read the current cache (proxy.readQuery
), add it our new item to the array of items, and then write back to the cache, which updated our UI.
Conclusion
GraphQL is becoming more and more mainstream. Much of the complexity surrounding GraphQL has to do with managing the back end and the API layer. However, tools like AppSync abstract away this complexity, freeing developers from spending most of their time configuring and working on the server.
I look forward to much more innovation in this space, and can't wait to see what else we will see in 2018!
If you're interested in using AppSync along with the Serverless framework, check out this great introduction to using the two of them together.
If you would like to learn more about AWS AppSync, I would suggest taking a look at the AppSync homepage and the documentation for building a GraphQL client.
If you want to contribute to this project, you can connect to our GitHub repo. If you have any ideas, feel free to send us a PR, or use this app as a starter for your next React Native GraphQL project!
And in the meantime, check out some of our other React Native tutorials here on Envato Tuts+!
- Mobile DevelopmentTools for React Native Development
- React NativePractical Animation Examples in React Native
- React NativeGet Started With React Native Layouts
- React10 React Native Applications for You to Use, Study, and Apply