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.
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/"
}
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.
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.
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.
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'
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.
This will download a file named AppSync.js
, copy the contents of this file and paste it inside src/global/config/index.js
file.
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.
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.
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.
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.
December 23, 2017 at 01:45 AM
and is written by Dhruv Kumar Jha (me).