Getting Started With The Frontend

The time has come, We will work on the frontend now.

We will make use of the famous create-react-app library along with graphql, react-router, aws-appsync and aws-appsync-react + some more.


Setting up our Project

If you don't know what Create React App is, Checkout this URL, You need it install it using npm globally. All the directions are provided on their Repository

Anyways, If you don't have create-reat-app installed run the command npm install -g create-react-app, This will install Create React App globally.

Now let's create our project using it, Open the directory where you want to place all the project files and run the command

create-react-app appsync-todo-app

This will generate boilerplate code for us as well as install required modules/packages.

Let's cd (go) into that directory and install all the other packages we require.

Run the command

yarn add react-apollo graphql-tag aws-sdk react-router-dom aws-appsync aws-appsync-react node-sass-chokidar

This will install all of our dependencies., We need to make few changes to our package.json file as well, So open that file and add the highlighted code in scripts section.

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject",
    "build-css": "node-sass-chokidar --include-path ./src/styles --include-path ./node_modules src/styles/ -o src/styles",
    "watch-css": "npm run build-css && node-sass-chokidar --watch --recursive --include-path ./src/styles --include-path ./node_modules src/styles/ -o src/styles/"
  }

Deleting Files We Don't Need

Create React App created many files which we don't need, So let's delete them., Delete these files

src/logo.svg
src/App.test.js
src/App.css
src/App.js
src/index.css

And while you're at it, Replace all the contents of README.md file with this (you can change the contents to suit your needs.)

## AWS AppSync Todo Application
Simple Todo List application built using AWS AppSync, Dynamo DB and React.

Creating Directories

Now we will structure our project so everything is well organized and easy to work with.

Let's start by creating the directories we need, Create all these directories.

/src/components
/src/components/app
/src/components/common
/src/components/layouts
/src/global
/src/global/config
/src/global/helper
/src/graphql
/src/graphql/mutations
/src/graphql/queries
/src/pages
/src/pages/users
/src/styles

We will place all of our components within the components directory. One might say everything in react is a component. (that's true.)

We will store all config and helper functions in global directory.

We will place all of our GraphQL related queries and mutations in graphql directory.

Our pages directory will hold all the pages we need.

And as the name says style directory will home all of our scss/css styles.


Creating Files

Now let's create all the (empty) files we need

Components

/src/components/app/Item.js
/src/components/app/List.js
/src/components/app/User.js

/src/components/common/Header.js
/src/components/common/Loading.js

/src/components/layouts/Default.js

Config File

/src/global/config/index.js

GraphQL Files

/src/graphql/mutations/createUser.js
/src/graphql/mutations/updateUser.js
/src/graphql/mutations/deleteUser.js

/src/graphql/queries/users.js
/src/graphql/queries/user.js

Application Pages

/src/pages/Dashboard.js
/src/pages/users/Create.js
/src/pages/users/Show.js
/src/pages/users/Form.js
/src/pages/users/Update.js

CSS Style

/src/styles/index.css

We will create more directories and files as needed, For now this will suffice.


Let's Simplify Module/File Imports.

I assume you know how we import packages in our files/components, Like

import Module from 'module-name'

But if it's not a package installed via npm then things get difficult and we import it like

import MyComponent from '../../../components/layouts/MyComponent'

This sucks, So let's uncomplicate this, Thankfully create-react-app makes this much easier.

Create a new file named .env and place it in the Root Directory and place this code inside it

NODE_PATH=src

Now we can call any file from any directory relative to the src directory.

// instead of this
import MyComponent from '../../../components/layouts/MyComponent'

// we can do (as long as components is in src directory)
import MyComponent from 'components/layouts/MyComponent'

Downloading our AppSync API Config File

Our client needs to communicate with GraphQL Backend and we have already created that in previous tutorials., But our frontend doesn't know about that.

So we nned to tell our frontend about our GraphQL Backend and we can do that easily by downloading the Config file provided by AWS AppSync.

Open AWS AppSync and click on the project we created TodoApplication

Scroll below the page and you can see iOS, Web, React Native tabs, Click on Web Tab. There you can see big orange button named Download click on that.

AWS AppSync Config File Download

This will download a file named AppSync.js, copy the contents of this file and paste it inside src/global/config/index.js file.


Writing Code for our GraphQL Schema Files

Open /src/graphql/mutations/createUser.js and place this code inside it

import gql from 'graphql-tag';

export default gql`
mutation ($name: String!, $email: String!, $password: String!, $phone: String, $address: String) {
  createUser(
    name: $name
    email: $email
    password: $password
    phone: $phone
    address: $address
  ) {
    __typename
    id
    name
    email
    phone
    address
  }
}`;

If you're wondering what __typename is, This is provided to us by GraphQL itself., It's the name of Type associated with this Mutation. In this case it's value will be User

Open /src/graphql/mutations/updateUser.js and place this code inside it

import gql from 'graphql-tag';

export default gql`
mutation ($id: ID!, $name: String, $email: String, $phone: String, $address: String) {
  updateUser(
    id: $id
    name: $name
    email: $email
    phone: $phone
    address: $address
  ) {
    __typename
    id
    name
    email
    phone
    address
  }
}`;

Open /src/graphql/mutations/deleteUser.js and place this code inside it

import gql from 'graphql-tag';

export default gql`
mutation ($id: ID!) {
  deleteUser(id: $id) {
    __typename
    id
    name
    email
  }
}`;

Open /src/graphql/queries/users.js and place this code inside it

import gql from 'graphql-tag';

export default gql`
query users {
  users {
    __typename
    id
    name
    email
    phone
    address
    lists {
      id
      name
      description
      items {
        id
        name
        description
        date
        status
      }
    }
  }
}`;

And finally open /src/graphql/queries/user.js and place this code inside it.

import gql from 'graphql-tag';

export default gql`
query user( $id: ID! ) {
  user( id: $id ) {
    __typename
    id
    name
    email
    phone
    address
    lists {
      id
      name
      description
      items {
        id
        name
        description
        date
        status
      }
    }
  }
}`;

This is the same schema we created in AppSync and we have already executed these queries.


Application Style

I decided not to use any Frameworks for this project, I do recommend you checkout Ant Design, It's my favorite framework.

Open /src/styles/index.css and place this code inside it.

@charset "UTF-8";
/* http://meyerweb.com/eric/tools/css/reset/
   v2.0 | 20110126
   License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline; }

/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
  display: block; }

body {
  line-height: 1; }

ol, ul {
  list-style: none; }

blockquote, q {
  quotes: none; }

blockquote:before, blockquote:after,
q:before, q:after {
  content: '';
  content: none; }

table {
  border-collapse: collapse;
  border-spacing: 0; }

/**
 * Mixins File
 */
body {
  font-family: "Segoe UI", "Open Sans", Tahoma, Arial, sans-serif;
  padding-top: calc( 54px + 40px); }

h1, h2, h3 {
  margin-bottom: 26px;
  font-weight: 700; }

h1 {
  font-size: 40px;
  line-height: 40px; }

h2 {
  font-size: 30px;
  line-height: 32px; }

h3 {
  font-size: 25px;
  line-height: 27px; }

.sub-heading {
  font-family: georgia, serif;
  margin-top: -20px;
  font-size: 20px;
  line-height: 20px;
  color: #666; }

.button,
a.button {
  font-size: 13px;
  line-height: 15px;
  font-weight: 700;
  color: #FFF !important;
  background: #333;
  outline: 0;
  text-decoration: none !important;
  border-radius: 2px;
  border: 1px solid;
  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
  padding: 4px 10px;
  display: inline-block;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
  cursor: pointer;
  -webkit-transition: all 0.3s ease;
  -moz-transition: all 0.3s ease;
  transition: all 0.3s ease; }
  .button:hover, .button:focus,
  a.button:hover,
  a.button:focus {
    color: #FFF !important; }
  .button:active,
  a.button:active {
    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.1); }
  .button.size--small,
  a.button.size--small {
    padding: 2px 4px; }
  .button.size--full,
  a.button.size--full {
    display: block; }
  .button.size--medium,
  a.button.size--medium {
    padding: 6px 20px; }
  .button.size--large,
  a.button.size--large {
    padding: 15px 30px; }
  .button.button--green,
  a.button.button--green {
    background: #009E0F; }
  .button.button--info,
  a.button.button--info {
    background: rgba(16, 142, 233, 0.82); }
    .button.button--info:hover,
    a.button.button--info:hover {
      box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.1); }
  .button.button--blue,
  a.button.button--blue {
    background: #2B78E4; }
  .button.button--yellow,
  a.button.button--yellow {
    background: #FFFF00;
    color: #000 !important; }
  .button.button--pink,
  a.button.button--pink {
    background: #FF4848; }
  .button.button--delete,
  a.button.button--delete {
    background: #C9302C;
    border-color: #C9302C; }

.error {
  font-size: 16px;
  line-height: 16px;
  padding: 20px 40px;
  background: #ffe3da;
  border: 1px solid #ffc3b0;
  margin: 30px 0;
  color: #d45831; }

.form--wrapper {
  margin: 40px 0; }

form .input {
  margin-bottom: 25px; }
  form .input:last-child {
    margin-bottom: 0; }

form input, form textarea, form select {
  font-family: Arial, Tahoma, ​sans-serif;
  font-size: 16px;
  line-height: 20px;
  padding: 10px 20px;
  border: 1px solid #CCC;
  display: block;
  width: 100%;
  cursor: pointer;
  border-radius: 3px; }

form label {
  display: block;
  font-size: 18px;
  line-height: 18px;
  margin-bottom: 5px;
  font-weight: 700;
  cursor: pointer; }

* {
  box-sizing: border-box; }

.layout--default {
  margin: 0 auto;
  width: 700px;
  max-width: 90%; }

.component--header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 54px;
  background: #3367d6;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28);
  padding: 0 20px;
  display: flex;
  justify-content: space-between; }
  .component--header h1 {
    font-size: 26px;
    color: #FFF;
    line-height: 54px; }
    .component--header h1 a {
      color: #FFF;
      text-decoration: none; }
  .component--header nav {
    display: flex; }
    .component--header nav a {
      display: block;
      font-size: 16px;
      font-weight: 600;
      text-decoration: none;
      text-transform: uppercase;
      align-self: center;
      padding: 0 15px;
      line-height: 54px;
      color: #FFF; }
    .component--header nav a.active {
      background: rgba(0, 0, 0, 0.2); }

.component--users {
  margin: 50px 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between; }
  .component--users .user {
    width: 49%;
    border: 1px solid #CCC;
    padding: 20px;
    margin-bottom: 20px;
    cursor: pointer;
    -webkit-transition: all 0.3s ease;
    -moz-transition: all 0.3s ease;
    transition: all 0.3s ease; }
    .component--users .user:hover {
      background: #ffffc5;
      border-color: #efef25; }
    .component--users .user .name {
      font-size: 24px;
      line-height: 24px;
      font-weight: 700; }
    .component--users .user .email {
      font-size: 15px;
      margin: 10px 0; }
    .component--users .user .todos {
      font-size: 20px;
      line-height: 20px; }
      .component--users .user .todos span {
        color: #999; }

.component--loading {
  text-align: center; }
  .component--loading .loader {
    margin: 0 auto;
    border: 16px solid #f3f3f3;
    border-top: 16px solid #3498db;
    border-radius: 50%;
    width: 60px;
    height: 60px;
    animation: spin 2s linear infinite; }

@keyframes spin {
  0% {
    transform: rotate(0deg); }
  100% {
    transform: rotate(360deg); } }
  .component--loading p {
    margin-top: 12px;
    font-size: 18px;
    line-height: 20px; }

.empty--data {
  margin: 30px 0;
  padding: 20px;
  border: 1px solid #CCC;
  font-size: 20px; }

.component--lists {
  margin-top: 30px; }
  .component--lists .list {
    margin-bottom: 30px;
    border: 1px solid #CCC; }
    .component--lists .list .empty {
      font-size: 20px;
      color: #666; }
    .component--lists .list > .title {
      padding: 10px 20px;
      background: #2196f3;
      font-size: 20px;
      font-weight: 700;
      color: #FFF; }
    .component--lists .list .items {
      padding: 20px; }
    .component--lists .list .item {
      padding-bottom: 20px;
      border-bottom: 1px solid #CCC;
      margin-bottom: 20px; }
      .component--lists .list .item:last-child {
        border-bottom: none;
        padding-bottom: 0;
        margin-bottom: 0; }
      .component--lists .list .item .title {
        font-weight: 700;
        color: #000; }
      .component--lists .list .item .description {
        margin-top: 10px;
        color: #999; }

Open /src/index.js and replace its code with

import React from 'react';
import ReactDOM from 'react-dom';
import 'styles/index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

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

We have just referenced our css file here.


Updating the App.js File

Open src/App.js file and replace its contents with

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

import AWSAppSyncClient from "aws-appsync";
import { Rehydrated } from 'aws-appsync-react';
import { ApolloProvider } from 'react-apollo';
import config from 'global/config';

import Layout from 'components/layouts/Default'
import DashboardPage from 'pages/Dashboard';

import ShowUserPage from 'pages/users/Show';
import CreateUserPage from 'pages/users/Create';
import UpdateUserPage from 'pages/users/Update';


const client = new AWSAppSyncClient({
  url: config.graphqlEndpoint,
  region: config.region,
  auth: {
    type: config.authenticationType,
    apiKey: config.apiKey,
  }
});

const App = () => (
  <Router>
    <Layout>
      <Route exact={true} path="/" component={ DashboardPage } />
      <Switch>
        <Route path="/users/create" component={ CreateUserPage } />
        <Route path="/users/:id/edit" component={ UpdateUserPage } />
        <Route path="/users/:id" component={ ShowUserPage } />
      </Switch>
    </Layout>
  </Router>
);

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

export default WithProvider;

What's going on in this file?

Everything.

First, We're importing the required modules to setup ApolloProvider, AppSync and then Routes.

const client = new AWSAppSyncClient({
  url: config.graphqlEndpoint,
  region: config.region,
  auth: {
    type: config.authenticationType,
    apiKey: config.apiKey,
  }
});

This code creates a new GraphQL Client, Which react-apollo module makes use of.

Here we're specifying our AppSync API Endpoint, Region, Auth Type and API Key

const App = () => (
  <Router>
    <Layout>
      <Route exact={true} path="/" component={ DashboardPage } />
      <Switch>
        <Route path="/users/create" component={ CreateUserPage } />
        <Route path="/users/:id/edit" component={ UpdateUserPage } />
        <Route path="/users/:id" component={ ShowUserPage } />
      </Switch>
    </Layout>
  </Router>
);

In the above code, We're setting all of our routes, Layout, When we add more routes, We will add them here.

</Switch> Makes sure only one of these Components is active at a time based on the URL.

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

This is the code that sets up GraphQL magic so our pages and components can easily ask for the data they need and it will be provided to them.

All other imports are for pages, components.

If you want me to explain in-depth about this code, how it works, etc do let me know (by Commenting or via Twitter.)

And with this we will call it a day.


Conclusion

We did lot of project setup in this tutorial, If you have any issues you can find the bleeding edge repository of this project at dhruv-kumar-jha/appsync-todo-app

I have also created a Live Demo (not completed), You can play with that as well and give me your feedback.

Have an awesome time and Merry Christmas + Happy Holidays.

See you soon.

Meta Information

This article was published on December 23, 2017 at 01:45 AM and is written by Dhruv Kumar Jha (me).
This is part of series: Building a Todo Application using GraphQL and AWS AppSync
Tags
GraphQL
React
AppSync
AWS AppSync
Create React App