DEV Community

John Demian
John Demian

Posted on

React + GraphQL app with Aws Amplify and AppSync are amazing!

This post is going to be a tad different and longer than what you are used to but I promise, it's going to be an interesting one. We are going to build a serverless React + GraphQL Web app with Aws amplify and AppSync.

What is Aws AppSync?

Aws AppSync helps us create a serverless backend for Android or IOS or Web apps.
It integrates with Amazon DynamoDB, Elasticsearch, Cognito, and Lambda, enabling you to create sophisticated applications, with virtually unlimited throughput and storage, that scale according to your business needs.

AppSync also enables real-time subscriptions as well as offline access to app data.

When an offline device reconnects, AppSync will syncs only the updates occurred while the device was offline and not the entire database.

How does AppSync Works?

aws app sync works

We'll create our GraphQL schema by using AppSync Visual editor or Amplify cli. Once that's done, AppSync takes cares of everything like enabling Dynamodb resources and creating resolver functions for our schema.

Getting Started with the Amplify Framework

First, we need to install the Amplify command line tool which is used to used to create and maintain serverless backends on AWS.

Run the below command to install the aws-amplify.

npm install -g @aws-amplify/cli
Enter fullscreen mode Exit fullscreen mode

Mac users need to use sudo before npm.

aws-amplify-cmd-tool installation

Once you have successfully installed it, you need to configure your AWS account by running the following command.

amplify configure
Enter fullscreen mode Exit fullscreen mode

Watch this video to configure your cli with your Aws account.

Create React App

Use the create-react-app to create the react app.

npx create-react-app awsgraphql-react
Enter fullscreen mode Exit fullscreen mode

The above command will download the required files in the "awsgraphql-react" folder to start the react app.

cd awsgraphql-react change the working directory.

Adding GraphQL Backend

Run the follow the command to initialize the new amplify project.

amplify init
Enter fullscreen mode Exit fullscreen mode

It prompts with different questions like choosing your favorite code editor and type of app you are building.

amplify-init

Now open your project folder in your code editor you will see a amplify folder and .amplifyrc file is added to your react app.

folder-structure-amplify.png

Once you successfully initialized the amplify project Its time to add an AppSync graphql API to our project by running the following command.

amplify add api
Enter fullscreen mode Exit fullscreen mode

This command will prompt with two options Rest or GraphQL choose GraphQL.

? Please select from one of the below-mentioned services (Use arrow keys)
❯ GraphQL
  REST
Enter fullscreen mode Exit fullscreen mode

Name your GraphQL endpoint and choose authorization type Api key.

? Please select from one of the below mentioned services GraphQL
? Provide API name: awsgraphqlreact
? Choose an authorization type for the API (Use arrow keys)
❯ API key
  Amazon Cognito User Pool
Enter fullscreen mode Exit fullscreen mode

Now you need to select the following options.

? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with
 ID, name, description)
? Do you want to edit the schema now? Yes
Enter fullscreen mode Exit fullscreen mode

Let's edit our schema before pushing it to the aws open your graphql schema which is located in the following folder amplify/backend/api/awsgraphqlreact/schema.graphql.

Remove everything and add the schema below.

type Post @model {
    id: ID!
    title: String!
    body:String!
    createdAt:String!
}
Enter fullscreen mode Exit fullscreen mode

This a Post object type with four fields ID,title,body and createdAt.

@model : This a model directive which tells amplify cli to store the following types in the dynamodb table.

Now run the below command to update your backend schema.

amplify push
Enter fullscreen mode Exit fullscreen mode

This command will prompt with following questions and choose yes for every question.

| Category | Resource name   | Operation | Provider plugin   |
| -------- | --------------- | --------- | ----------------- |
| Api      | awsgraphqlreact | Create    | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully. Edit your schema at /Users/saigowtham/Desktop/awsgraphql-react/amplify/backend/api/awsgraphqlreact/schema.graphql
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations
and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations
- queries,mutations and subscriptions Yes
Enter fullscreen mode Exit fullscreen mode

If you open your aws console https://console.aws.amazon.com/appsync/ you can see a complete schema file with queries , mutations and resolver functions which is created by the aws-amplify cli by using our Post object type.

backend-graphql-schema

Connecting GraphQL Api to React

Now we are connecting our GraphQL backend with the react app for this first we need to download the the following packages.

npm install aws-appsync graphql-tag react-apollo
Enter fullscreen mode Exit fullscreen mode

Once you successfully installed, now open your index.js file in your react app and add the below configuration.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import gql from 'graphql-tag';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import aws_config from './aws-exports';

const client = new AWSAppSyncClient({
    url: aws_config.aws_appsync_graphqlEndpoint,
    region: aws_config.aws_appsync_region,
    auth: {
        type: AUTH_TYPE.API_KEY,
        apiKey: aws_config.aws_appsync_apiKey,
    }
});

ReactDOM.render(<App />, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

After that we import the AWSAppSyncClient constructor, AUTH_TYPE from the aws-appsync package and aws_config from the ./aws-exports file which is created automatically by the amplify cli.

Next, we'll have to instantiate the new AWSAppSyncClient client by passing the aws_config.

Running the first query

In graphql 'query' is used to fetch the data from the graphql endpoint.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import gql from 'graphql-tag';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import aws_config from './aws-exports';

import { listPosts } from './graphql/queries';

const client = new AWSAppSyncClient({
    url: aws_config.aws_appsync_graphqlEndpoint,
    region: aws_config.aws_appsync_region,
    auth: {
        type: AUTH_TYPE.API_KEY,
        apiKey: aws_config.aws_appsync_apiKey,
    }
});

client.query({
    query: gql(listPosts)
}).then(({ data }) => {
    console.log(data);
});

ReactDOM.render(<App />, document.getElementById('root'));

Enter fullscreen mode Exit fullscreen mode

In the code above, we invoke the client.query method by passing a listPosts query which is generated automatically by the aws-amplify based on our graphql endpoint.

You'll find the data of this query logged inside your browser console.

client-query

Since we don't have any data in our dynamodb table so that we got 0 items, which is what we should expect.

Let's use the 'react-apollo' to run the queries and mutations from the UI.

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import aws_config from './aws-exports';
import { ApolloProvider } from 'react-apollo'

const client = new AWSAppSyncClient({
    url: aws_config.aws_appsync_graphqlEndpoint,
    region: aws_config.aws_appsync_region,
    auth: {
        type: AUTH_TYPE.API_KEY,
        apiKey: aws_config.aws_appsync_apiKey,
    }
});



ReactDOM.render(<ApolloProvider client={client}>
    <App />
</ApolloProvider>, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

Next we import an ApolloProvider component from the 'react-apollo' and wrap it in our App component by passing a client so that we can access the client anywhere from our react app.

Creating a Post

We need to create a new component called CreatePost in the createPost.js file which helps us to run the Mutation and add data to our backend.

createPost.js

import React from "react";
import { Mutation } from "react-apollo";
import { createPost } from "./graphql/mutations";
import gql from "graphql-tag";

class CreatePost extends React.Component {
  handleSubmit = (e, createPost) => {
    e.preventDefault();
    createPost({
      variables: {
        input: {
          title: this.title.value,
          body: this.body.value,
          createdAt: new Date().toISOString()
        }
      }
    }).then(res => {
      this.title.value = "";
      this.body.value = "";
    });
  };
  render() {
    return (
      <div>
        <h1>Create post</h1>

        <Mutation mutation={gql(createPost)}>
          {(createPost, { data, loading, error }) => {
            return (
              <div>
                <form
                  className="add-post"
                  onSubmit={e => this.handleSubmit(e, createPost)}
                >
                  <input
                    type="text" placeholder="Title"
                    ref={node => (this.title = node)}
                    required
                  />
                  <textarea
                    rows="3"
                    cols="40"
                    placeholder="Body"
                    ref={node => (this.body = node)}
                    required
                  />
                  <button>{loading ? "Yes boss..." : "Create Post"}
                  </button>
                </form>
                {error && <p>{error.message}</p>}
              </div>
            );
          }}
        </Mutation>
      </div>
    );
  }
}

export default CreatePost;
Enter fullscreen mode Exit fullscreen mode

In CreatePost we have imported a Mutation component from the 'react-apollo' and gql from the 'graphql-tag'. Then createPost mutation is imported from ./grahql/mutations file.

The 'createPost' mutation takes three dynamic arguments which are title, body, createdAt.

title: The title of our post.

body: The body of our post.

createdAt: Post creation time and date.

In your App.js import the createPost component.

App.js

import React, { Component } from 'react';
import CreatePost from './createPost';

class App extends Component {
  render() {
    return (
      <div className="App">
        <CreatePost />
      </div>
    );
  }
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Let's test our createPost component by creating our first post.

create-post-mutation

Open your aws-console to see your data is stored inside the DynamoDB table.

Fetching the Data

Currently, we are not rendering any data on the UI so let's query a data to GraphQL endpoint so that we can see newly created posts.

We'll need to create two new components.

post.js

import React from 'react';

class Post extends React.Component {

    componentDidMount() {
        this.props.subscribeToMore();
    }


    render() {
        const items = this.props.data.listPosts.items;

        return items.map((post) => {
            return (
                <div>
                    <h1>{post.title}</h1>
                    <p>{post.body}</p>
                    <time dateTime={post.createdAt}>
                    {new Date(post.createdAt).toDateString()}</time>
                    <br />
                </div>

            )
        })


    }

}


export default Post;
Enter fullscreen mode Exit fullscreen mode

displayPosts.js

import React from 'react'
import { Query } from 'react-apollo'
import { listPosts } from './graphql/queries';
import { onCreatePost } from './graphql/subscriptions'
import gql from 'graphql-tag';
import Post from './post'

class DisplayPosts extends React.Component {

    subsCribeNewPosts = (subscribeToMore) => {
        return subscribeToMore({
            document: gql(onCreatePost),
            updateQuery: (prev, { subscriptionData }) => {
                if (!subscriptionData.data) return prev;
                const newPostData = subscriptionData.data.onCreatePost;
                return Object.assign({}, prev, {
                    listPosts: {
                        ...prev.listPosts,
                        items: [...prev.listPosts.items, newPostData]
                    }
                })
            }
        })
    }


    render() {
        return (
            <div className="posts">
                <Query query={gql(listPosts)}  >
                    {({ loading, data, error, subscribeToMore }) => {

                        if (loading) return <p>loading...</p>
                        if (error) return <p>{error.message}</p>

                        return <Post data={data} subscribeToMore={() =>
                            this.subsCribeNewPosts(subscribeToMore)} />
                    }}
                </Query>



            </div>
        )
    }
}


export default DisplayPosts;
Enter fullscreen mode Exit fullscreen mode

In the DisplayPosts component, we query the list of posts and also enable real time subscriptions so that we can see newly created posts rendered first.

Inside the Query component, we access the subscribeToMore function and pass it to the subscribeNewPosts method.

subscribeToMore: it is invoked whenever the Post component is mounted to the dom and listen for the new posts added to our graphql API.

updateQuery: the updateQuery function is used to merge the previous data and current data.

Update your App.js file by importing the DisplayPostscomponent.

App.js

import React, { Component } from 'react';
import CreatePost from './createPost';
import DisplayPosts from './displayPosts';

class App extends Component {
  render() {
    return (
      <div className="App">
        <CreatePost />
        <DisplayPosts />
      </div>
    );
  }
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Let's test our DisplayPosts component by creating new posts.

real-time-subcriptions.gif

In the above image, we tested it by opening two new browser windows.

Edit Post

Let's create the EditPost component which helps us to edit the previously created post.

editPost.js

import React from "react";
import { updatePost } from "./graphql/mutations";
import { Mutation } from "react-apollo";
import gql from "graphql-tag";

class EditPost extends React.Component {
  state = {
    show: false,
    postData: {
      title: this.props.title,
      body: this.props.body
    }
  };

  handleModal = () => {
    this.setState({ show: !this.state.show });
    document.body.scrollTop = 0;
    document.documentElement.scrollTop = 0;
  };

  handleSubmit = (e, updatePost) => {
    e.preventDefault();
    updatePost({
      variables: {
        input: {
          id: this.props.id,
          title: this.state.postData.title,
          body: this.state.postData.body
        }
      }
    }).then(res => this.handleModal());
  };

  handleTitle = e => {
    this.setState({
      postData: { ...this.state.postData, title: e.target.value }
    });
  };

  handleBody = e => {
    this.setState({
      postData: { ...this.state.postData, body: e.target.value }
    });
  };

  render() {
    return (
      <>
        {this.state.show && (
          <div className="modal">
            <button className="close" onClick={this.handleModal}>
              X
            </button>
            <Mutation mutation={gql(updatePost)}>
              {updatePost => {
                return (
                  <form
                    className="add-post"
                    onSubmit={e => this.handleSubmit(e, updatePost)}
                  >
                    <input
                      type="text"
                      required
                      value={this.state.postData.title}
                      onChange={this.handleTitle}
                    />
                    <textarea
                      rows="3"
                      cols="40"
                      required
                      value={this.state.postData.body}
                      onChange={this.handleBody}
                    />
                    <button>Update Post</button>
                  </form>
                );
              }}
            </Mutation>
          </div>
        )}
        <button onClick={this.handleModal}>Edit</button>
      </>
    );
  }
}

export default EditPost;

Enter fullscreen mode Exit fullscreen mode

In EditPost we are going to import the Mutation component,updatePost mutation and gql tag then we use the Mutation component by passing the mutation prop.

In the Mutation component, we need to pass the function as children because it is using the render props pattern.

The first parameter of the function is the mutation function so that we passed this function as an argument to the handleSubmit method and invoked with the updated post title and body.

Open your post.js file and add the EditPost component.

post.js

import React from 'react';
import EditPost from './editPost'

class Post extends React.Component {

    componentDidMount() {
        this.props.subscribeToMore();
    }


    render() {
        const items = this.props.data.listPosts.items;

        return items.map((post) => {
            return (
                <div>
                    <h1>{post.title}</h1>
                    <p>{post.body}</p>
                    <time dateTime={post.createdAt}>
                    {new Date(post.createdAt).toDateString()}</time>
                    <br />
                    <EditPost {...post} />
                </div>

            )
        })


    }

}

export default Post;
Enter fullscreen mode Exit fullscreen mode

Let's test our EditPost component by editing any previously created post.

edit-post-mutation

DeletePost

Now we are implementing DeletePost component with Optimistic UI.

What is Optimistic UI?

For example, if we delete a Post, it takes time to get the response from the server, and only then can we update the UI. With Optimistic UI we can render this component and once we got a response from the server we replace the optimistic result with actual server result.

Create a new file called deletePost.js.

deletePost.js

import React, { Component } from 'react'
import { Mutation } from 'react-apollo';
import { deletePost } from './graphql/mutations';
import gql from 'graphql-tag';
import { listPosts } from './graphql/queries';


class DeletePost extends Component {

    handleDelete = (deletePost) => {
        deletePost({
            variables: {
                input: {
                    id: this.props.id
                }
            },
            optimisticResponse: () => ({
                deletePost: {
                    // This type must match the return type of
                    //the query below (listPosts)
                    __typename: 'ModelPostConnection',
                    id: this.props.id,
                    title: this.props.title,
                    body: this.props.body,
                    createdAt: this.props.createdAt
                }
            }),
            update: (cache, { data: { deletePost } }) => {
                const query = gql(listPosts);

                // Read query from cache
                const data = cache.readQuery({ query });

                // Add updated postsList to the cache copy
                data.listPosts.items = [
                    ...data.listPosts.items.filter(item =>
                     item.id !== this.props.id)
                ];

                //Overwrite the cache with the new results
                cache.writeQuery({ query, data });
            }
        })
    }

    render() {
        return (
            <Mutation mutation={gql(deletePost)}>
                {(deletePost, { loading, error }) => {
                    return <button onClick={
                       () => this.handleDelete(deletePost)}>
                        Delete Post</button>
                }}
            </Mutation>
        )
    }
}


export default DeletePost;
Enter fullscreen mode Exit fullscreen mode

In optimisticResponse function we passed exactly the delete Post data with __typename:'ModelPostConnection' then we update the cache by removing the deleted post.

Update your post.js file by adding DeletePost component.

post.js

import React from 'react';
import EditPost from './editPost'
import DeletePost from './deletePost'

class Post extends React.Component {

    componentDidMount() {
        this.props.subscribeToMore();
    }

    render() {
        const items = this.props.data.listPosts.items;

        return items.map((post) => {
            return (
                <div key={post.id}>
                    <h1>{post.title}</h1>
                    <p>{post.body}</p>
                    <time dateTime={post.createdAt}>{
                        new Date(post.createdAt).toDateString()}</time>
                    <br />
                    <EditPost {...post} />
                    <DeletePost {...post} />
                </div>

            )
        })
    }
}

export default Post;
Enter fullscreen mode Exit fullscreen mode

delete-mutation

In the above, we have tested it in offline mode but we can see the UI is updated instantly through an “optimistic response” once we got online appsync send a deletePost mutation to update our backend.

Hosting the React app

By using amplify-cli we can also host our react app in Aws s3 bucket and CloudFront.

Open your terminal and run following command.

amplify hosting add
Enter fullscreen mode Exit fullscreen mode

hosting-cmd

published url

Code repository

For monitoring, debugging and error detection of AWS Lambdas we are using Dashbird.

Why Dashbird?

  • Dashbird helps us actively monitoring the health and errors.

  • One main thing about Dashbird is its user-friendly Interface.

  • Dashbird visualizes all your AWS Lambda metrics like
    memory utilization, invocation count, and execution duration.

DashBird Interface

dashbird-interface

I know, this was an extremely long post and I have to congratulate you for sticking with it. Since you took the time to read all of it I'd love to hear your thoughts. Please leave a comment letting me know what you liked or disliked about it.


Mad props to Sai for creating such a massive and comprehensive tutorial. We look forwrd to reading his next one. Check out his website here.

I've had this originally posted on the Dashbird blog and since it was so popular there I figured you guys might like it too.

Top comments (2)

Collapse
 
alejandromacari profile image
Alejandro Macari

Can AWS Amplify be used totally offline? I'm making an electron app with a free tier that will only store data locally and a premium tier that will sync with a cloud service for back-up and other goodies. But can't find if amplify and appsync can be used totally offline.

Collapse
 
j2rguez profile image
J2RGEZ • Edited

As far as I know, you can't (please let me know if I'm wrong). I recommend you take a look at the Booster Framework: booster.cloud/. It simplifies a lot of the development time, and it's getting close to finishing the local provider. I hope this helps in your case!