In previous tutorial we created a basic structure for our application, Installed the required packages and deleted the files we didn't need.
Now let's continue from there and Add Components, Pages for our basic CRUD operations.
We need a Layout Component (which we have specified in App.js
Router component), This layout will apply to all the routes (pages) and it will contain a Header
component as well.
Create a file names /src/components/layouts/Default.js
and place this code inside it.
import React from 'react';
import Header from 'components/common/Header';
const DefaultLayout = (props) => {
return (
<div className="layout--default">
<Header />
<div className="content">
{/* this will render our page content */}
{ props.children }
</div>
</div>
);
}
export default DefaultLayout;
This just creates few div's with class names so we can style it as well as includes Header component so header will be present in all the pages.
Let's add the Header component, Create a file /src/components/common/Header.js
and place this code inside it.
import React from 'react';
import { NavLink } from "react-router-dom";
const Header = (props) => {
return (
<div className="component--header">
<h1><NavLink to="/">Todo Application</NavLink></h1>
<nav>
<NavLink to="/" exact={true} activeClassName="active">Home</NavLink>
<NavLink to="/users/create" activeClassName="active">Add New User</NavLink>
</nav>
</div>
);
}
export default Header;
In this component we're importing the NavLink
component form react-router-dom
. This will allow us to add Links as well as assign the CSS Class name it should have when it's active, Making it easier for us to implement normal and active states for our links.
The Loading Component
We need a Loading component to display Loading indicator whenever we're making Network/API/GraphQL requests. This will help because we don't know how long it will take for the request to be completed and showing an Indicator to the user that something is going on behind the scenes will help with the User Experience.
Create a file /src/components/common/Loading.js
and place this code inside it.
import React from 'react';
const Loading = (props) => {
return (
<div className="component--loading" style={props.style}>
<div className="loader"></div>
<p>{ props.message ? props.message : 'Loading data please wait...' }</p>
</div>
);
}
export default Loading;
If a message
is passed to the component, It displays that otherwise displays a default message., As simple as that.
Also know as the Read Operation
This page will display all the users present in the Database as well as Link to different pages such as Create new User, View user details, Update user, etc.
Create a file /src/pages/Dashboard.js
and place this code inside it.
import React from 'react';
import { Link } from "react-router-dom";
import { graphql } from "react-apollo";
import UsersQuery from "graphql/queries/users";
import User from "components/app/User";
import Loading from 'components/common/Loading';
const DashboardPage = (props) => {
return (
<div>
<h1>Hello Admin, Welcome</h1>
<p className="sub-heading">These are all the users present in the database.</p>
{ props.data.users && props.data.users.length > 0 &&
<div className="component--users">
{ props.data.users.map( user => <User key={user.id} data={user} /> ) }
</div>
}
{ props.data.users && props.data.users.length < 1 &&
<div className="empty--data">No User(s) has been added yet.. Or someone just deleted all the users.</div>
}
{ ! props.data.users &&
<Loading style={{ marginTop: 50, marginBottom: 50 }} />
}
<div style={{ marginTop: 30 }}>
<Link to="/users/create" className="button size--large">Add New User</Link>
</div>
</div>
);
}
export default graphql(UsersQuery)(DashboardPage);
What's going on in this File?
import { graphql } from "react-apollo";
import UsersQuery from "graphql/queries/users";
...
export default graphql(UsersQuery)(DashboardPage);
We have imported the graphql
method from react-apollo
as well as our UsersQuery
which will fetch all the users from our database (i..e GraphQL Backend).
And in the last line we're using graphql
as Higher Order Component, Passing it our UsersQuery
and DashboardPage
component.
This will make the users
available in DashboardPage
component props.
We're also making use of the Loading component to show a loading indicator if users
is not present in props.data
.
Besides this, We're making use of a component named User
which displays each user., Let's create this component.
Create a file /src/components/app/User.js
and place this code inside it.
import React from 'react';
import { withRouter } from 'react-router'
const User = (props) => {
const { id, name, email, lists } = props.data;
const openUsersPage = () => {
props.history.push(`/users/${ id }`);
}
return (
<div className="user" onClick={ openUsersPage }>
<div className="name">{ name }</div>
<div className="email">{ email }</div>
<div className="todos">Total Todos: <span>{ lists.length }</span></div>
</div>
);
}
export default withRouter(User);
This just displays users details.
If there's one thing that's common between our Create User and Update User pages, Its the Form fields used in both of these pages.
Create a file /src/pages/users/Form.js
and place this code inside it.
import React from 'react';
import Loading from 'components/common/Loading';
class Form extends React.Component {
constructor() {
super();
this.state = {
loading: false,
error: false,
user: {}
}
this.hadleInputChange = this.hadleInputChange.bind(this);
this.submit = this.submit.bind(this);
}
hadleInputChange( name, event ) {
this.setState({ user: { ...this.state.user, [name]: event.target.value } });
}
submit(e) {
e.preventDefault();
if ( ! this.props.user && ( ! this.state.user.name || ! this.state.user.email || ! this.state.user.password ) ) {
this.setState({ error: true });
return;
}
else if ( this.props.user && this.props.user.id ) {
this.setState({ error: false });
}
else {
this.setState({ error: false });
}
if ( this.props.user && this.props.user.id ) {
this.setState({ error: false, loading: true });
this.props.submit({ ...this.props.user, ...this.state.user });
}
else {
this.setState({ loading: true });
this.props.submit( this.state.user );
}
}
render() {
const user = this.props.user || this.state.user;
return (
<div>
{ this.state.error &&
<div className="error">Please enter all the required fields.</div>
}
<form className="form--wrapper" method="POST" onSubmit={ this.submit } autoComplete="off">
{ this.state.loading &&
<Loading message={ user.id ? "Updating user details..." : "Adding new user.. please wait." } />
}
{ ! this.state.loading &&
<div>
<div className="input">
<label htmlFor="name">Full Name (required)</label>
<input type="text" id="name" name="name" defaultValue={ user.name ? user.name : '' } placeholder="John Doe" onChange={ (e) => { this.hadleInputChange('name', e) } } />
</div>
<div className="input">
<label htmlFor="email">Email Address (required)</label>
<input type="text" id="email" name="email" defaultValue={ user.email ? user.email : '' } placeholder="john.doe@gmail.com" onChange={ (e) => { this.hadleInputChange('email', e) } } />
</div>
{ ! user.id &&
<div className="input">
<label htmlFor="password">Password (required)</label>
<input type="password" id="password" name="password" defaultValue={ user.password ? user.password : '' } placeholder="Passw0rd" onChange={ (e) => { this.hadleInputChange('password', e) } } />
</div>
}
<div className="input">
<label htmlFor="phone">Phone Number</label>
<input type="text" id="phone" name="phone" defaultValue={ user.phone ? user.phone : '' } placeholder="+1 111 2222 333" onChange={ (e) => { this.hadleInputChange('phone', e) } } />
</div>
<div className="input">
<label htmlFor="address">Address</label>
<input type="text" id="address" name="address" defaultValue={ user.address ? user.address : '' } placeholder="Springfield" onChange={ (e) => { this.hadleInputChange('address', e) } } />
</div>
<div className="input">
<button type="submit" className="button size--large button--green">{ user.id ? 'Update User' : 'Add User' }</button>
</div>
</div>
}
</form>
</div>
)
}
}
export default Form;
This component takes in users
data as props, But it's optional., If user is specified then this form treats itself as an Update form and changes the button text, loading components message, form fields values otherwsie it treats itself as a Create form and uses the defaults.
hadleInputChange
method takes in the field name and the event, And it sets the field: value
in the state.
submit
method checks if all the fields we care about are filled or not, If not then it shows an error otherwise it submits the form by calling the submit
method in its props.
This way Create Page and Update Page can perform different actions which our Form component doesn't care for.
Also known as the Create Operation
Create a file /src/pages/users/Create.js
and place this code inside it.
import React from 'react';
import { graphql } from "react-apollo";
import createUserMutation from "graphql/mutations/createUser";
import UsersQuery from "graphql/queries/users";
import Form from "./Form";
const CreateUserPage = (props) => {
const handleSubmit = ( data ) => {
console.log('handleSubmit',data);
props.mutate({
variables: data,
optimisticResponse: {
__typename: 'Mutation',
createUser: {
__typename: 'User',
id: Math.random().toString(36).substring(7),
name: data.name,
email: data.email,
phone: data.phone || '',
address: data.address || '',
lists: [],
},
},
refetchQueries: [{ query: UsersQuery }],
update: (proxy, { data: { createUser } }) => {
const query = UsersQuery;
const data = proxy.readQuery({ query });
data.users.push(createUser);
proxy.writeQuery({ query, data });
},
})
.then( res => {
props.history.push('/');
})
.catch( res => {
if ( res.graphQLErrors ) {
const errors = res.graphQLErrors.map( error => error.message );
console.log('errors',errors);
}
});
}
return (
<div>
<h1>Add New User</h1>
<p className="sub-heading">Please enter the details below to add new user.</p>
<Form submit={ handleSubmit } />
</div>
)
}
export default graphql(createUserMutation)(CreateUserPage);
This component uses createUserMutation
for creating new user and UsersQuery
for refetching all the users from our GraphQL Server.
This component also uses Form component in its children and passes a custom submit
method named handleSubmit
to it.
The handleSubmit
method creates a GraphQL Mutation., But what's going on behind the scenes?
props.mutate({
variables: data,
optimisticResponse: {
__typename: 'Mutation',
createUser: {
__typename: 'User',
id: Math.random().toString(36).substring(7),
name: data.name,
email: data.email,
phone: data.phone || '',
address: data.address || '',
lists: [],
},
},
refetchQueries: [{ query: UsersQuery }],
update: (proxy, { data: { createUser } }) => {
const query = UsersQuery;
const data = proxy.readQuery({ query });
data.users.push(createUser);
proxy.writeQuery({ query, data });
},
})
.then( res => {
props.history.push('/');
})
.catch( res => {
if ( res.graphQLErrors ) {
const errors = res.graphQLErrors.map( error => error.message );
console.log('errors',errors);
}
});
variables
is the data passed to us by our Form component, These are the field: value
mapping of users data., We pass it to our GraphQL Backend.
optimisticResponse
is used for doing something that mimics server response even before we receive the response from the server, Making it seem like the user has been added Instantly. This is just the structure of the data we expect to see returned from the server.
refetchQueries
is used to specify the Queries we want to re-fetch as the data has been updated in the Backend.
update
this method is called while the server request is being made as well as after receiving the response from the server.
This will first add the data it sees in the optimisticResponse
and later when it receives the actual response from server, It will replace/update that data with the response returned from server.
If you have any doubts just Google or let me know.
Also know as the Update Operation
Create a file /src/pages/users/Update.js
and place this code inside it.
import React from 'react';
import { graphql, compose } from "react-apollo";
import updateUserMutation from "graphql/mutations/updateUser";
import UserQuery from "graphql/queries/user";
import UsersQuery from "graphql/queries/users";
import Form from "./Form";
import Loading from 'components/common/Loading';
const UpdateUserPage = (props) => {
const handleSubmit = ( data ) => {
props.mutate({
variables: data,
optimisticResponse: {
__typename: 'Mutation',
updateUser: {
__typename: 'User',
id: data.id,
name: data.name,
email: data.email,
phone: data.phone || '',
address: data.address || '',
lists: data.items || [],
},
},
refetchQueries: [{ query: UsersQuery }],
update: (proxy, { data: { updateUser } }) => {
const query = UsersQuery;
const data = proxy.readQuery({ query });
data.users = data.users.map(user => user.id !== updateUser.id ? user : { ...user, ...updateUser });
proxy.writeQuery({ query, data });
},
})
.then( res => {
props.history.push('/');
})
.catch( res => {
if ( res.graphQLErrors ) {
const errors = res.graphQLErrors.map( error => error.message );
console.log('errors',errors);
}
});
}
if ( ! props.data.user ) {
return (<Loading message="Loading user details..." />);
}
const { id, name, email, phone, address } = props.data.user;
return (
<div>
<h1>Update User: { props.data.user.name }</h1>
<p className="sub-heading">Please make the changes below and click on update user.</p>
<Form user={{ id, name, email, phone, address }} submit={ handleSubmit } />
</div>
)
}
export default compose(
graphql( UserQuery, { options: (props) => {
return { variables: { id: props.match.params.id }
} } } ),
graphql(updateUserMutation)
)(UpdateUserPage);
Most of the things are like what you've already seen., Except here we're passing the user data to the Form
component., And instead of createUserMutation
we're using updateUserMutation
.
Also known as the Read + Delete Operations
This page will be responsible for displaying users data as well as linking to other pages like Update User which we created previously and Delete user functionality.
Create a file /src/pages/users/Show.js
and place this code inside it.
import React from 'react';
import { Link } from "react-router-dom";
import { graphql, compose } from "react-apollo";
import UserQuery from "graphql/queries/user";
import UsersQuery from "graphql/queries/users";
import Loading from 'components/common/Loading';
import deleteUserMutation from "graphql/mutations/deleteUser";
import List from 'components/app/List';
const ShowUserPage = (props) => {
const deleteUser = () => {
if ( window.confirm('Are you sure you want to delete this user?') ) {
props.mutate({
variables: { id: props.match.params.id },
optimisticResponse: {
__typename: 'Mutation',
deleteUser: {
__typename: 'User',
id: props.data.user.id,
name: props.data.user.name,
email: props.data.user.email,
},
},
refetchQueries: [{ query: UsersQuery }],
update: (proxy, { data: { deleteUser } }) => {
const query = UsersQuery;
const data = proxy.readQuery({ query });
data.users = data.users.filter(user => user.id !== deleteUser.id);
proxy.writeQuery({ query, data });
},
})
.then( res => {
props.history.push('/');
})
.catch( res => {
console.log('res',res);
});
}
}
if ( props.data.user ) {
const { user } = props.data;
return (
<div>
<h1>{ user.name }'s Todos</h1>
<p className="sub-heading">These are all the todos associated with this user.</p>
<div style={{ marginTop: 20 }}>
<Link to={`/users/${user.id}/edit`} className="button button--yellow size--medium">Update User Details</Link>
<button
className="button button--danger size--medium"
style={{ marginLeft: 20 }}
onClick={ deleteUser }
>Delete this User</button>
</div>
{ user.lists && user.lists.length > 0 &&
<div className="component--lists">
{ user.lists.map( list => <List key={list.id} data={list} /> ) }
</div>
}
{ user.lists && user.lists.length < 1 &&
<div className="empty--data">No List(s) has been added by this User.</div>
}
</div>
);
}
else {
return (
<Loading />
);
}
}
export default compose(
graphql( UserQuery, { options: (props) => {
return { variables: { id: props.match.params.id }
} } } ),
graphql(deleteUserMutation)
)(ShowUserPage);
Here we're using UserQuery
to fetch details about a Single User and we're using deleteUserMutation
for deleting the user data from Database.
Other than that we're looping through the users Lists and displaying it using the component List
, If a user has no list we're showing a message saying No list found for this user.
Let's create our List
component
Create a file /src/components/app/List.js
and place this code inside it.
import React from 'react';
import Item from 'components/app/Item';
const List = (props) => {
const { data: list } = props;
return (
<div className="list">
<div className="title">{ list.name }</div>
<div className="items">
{ list.items && list.items.length > 0 &&
list.items.map( item => <Item key={item.id} data={ item } /> )
}
{ list.items && list.items.length < 1 &&
<div className="empty">No Item(s) has been added for this List.</div>
}
</div>
</div>
);
}
export default List;
This component just displays the List name as well as it's items using the Item
component, If the items array/data is empty, It shows a message saying No items added for this list
Now let's create our Item
component.
Create a file /src/components/app/Item.js
and place this code inside it.
import React from 'react';
const Item = (props) => {
const { data: item } = props;
return (
<div className="item">
<div className="title">{ item.name }</div>
<div className="description">{ item.description }</div>
</div>
);
}
export default Item;
Again this is a simple component, Just displays the title
and description
passed to it.
And with this we're done with the basic CRUD operations.
With this tutorial, I consider this series to be completed.
By now you should have the skills to create CRUD
operations for Lists
and Items
easily., And if you encounter any issues I am here to help.
I know we didn't touch Authentication
in this series and Authentication is very very Important, For that I will write another tutorial/series going indepth into Amazon Cognito + User Pools
and how to integrate it with React+GraphQL apps.
This has been an Awesome series for me to write, This was the first time I used Dynamo DB
and Velocity Template Language
., Overall an Awesome experience with AWS AppSync
.
If you enjoyed reading the series, Do subscribe to my Newsletter, You can also do my work for me and give me ideas and suggestions for new tutorials.
Thank you all for reading and see you soon.
December 30, 2017 at 11:11 AM
and is written by Dhruv Kumar Jha (me).