Performing the Basic Create, Read, Update and Delete Operations with GraphQL

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.


The Layout Component

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.


Creating the Header and Loading Components

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.


The Dashboard Page

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.


The Form Component

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.


Create User Page

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.


Update User Page

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.


The Show + Delete User Page

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.


Conclusion

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.

Meta Information

This article was published on December 30, 2017 at 11:11 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