Can I Use This?

This feature has been available since Webiny v5.40.0 and is available in Business & Enterprise tier.

What you will learn
  • how to use Amplify UI in a React app to build a sign-up and login form
  • how to build a notes React application where users can create, read, and delete notes after signing up

Building a Notes App in React With AWS Amplify UI
anchor

Great, we’re all set on the Webiny side. Now, let’s move on to building the Notes App in React using the AWS Amplify UI Authenticatorexternal link.

Demo Application Preview
anchor

Here’s a preview of the React application we’ll be building.

The code covered in this tutorial is also available in our GitHub examples repositoryexternal link.

Setup and Configuration
anchor

  1. Create a new React app: If you haven’t already, start by creating a new React app by running npx create-react-app my-notes-app.

  2. Install dependencies: Navigate to your project directory with cd my-notes-app command` and install AWS Amplify and Apollo client libraries.

    npm install @aws-amplify/ui-react aws-amplify @apollo/client
  3. Amplify Configuration: Configure Amplify with your AWS account details. Create an aws-config.js file in your src directory with the following configuration, replacing the placeholders with your actual AWS settings.

You can find these config values from this step.

If you lost these values, don’t worry. You can also retrieve them from the your-webiny-project-root/.pulumi/apps/core/.pulumi/stacks/core/dev.json file within your Webiny project.

Since we’ve deployed the dev environment for the demo, we’re seeing the dev.json file. If you’ve deployed your project in a different environment, you’ll find a file named after your environment, such as your-env-name.json stack file.

Please note these config values will have a different name in your stack file (e.g. in dev.json file) in Webiny project.

  • aws_cognito_region is notesAppUserPoolRegion
  • aws_user_pools_id is notesAppUserPoolId
  • aws_user_pools_web_client_id is notesAppUserPoolClient
  • aws_project_region is region
aws-config.js
const config = {
    "aws_project_region": "us-east-1",
    "aws_cognito_region": "us-east-1",
    "aws_user_pools_id": "us-east-1_XXXXXXX",
    "aws_user_pools_web_client_id": "XXXXXXXXXXXXXXXXXXXXX",
};

export default config;
  1. Initialize Amplify: In your index.js, import and configure Amplify.
index.js
import App from './App';
import { Amplify } from 'aws-amplify';import config from './aws-config';Amplify.configure(config);
const root = ReactDOM.createRoot(document.getElementById('root'));

Styling
anchor

Utilize the provided App.css file for styling your application. It includes styles for headers, notes listings, and animations. Update your App.css file with the following content.

App.css

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

h2 {
  text-align: center;
}

[data-amplify-container] {
  margin-top: 70px;
}

/* App.css */

.header {
  background-color: #293845;
  color: #fff;
  padding: 20px;
  text-align: center;
  margin-bottom: 20px;
  display: flex;
  justify-content: space-between; /* Aligns items to each end of the flex container */
  align-items: center; /* Aligns items vertically */
}

.header h1 {
  margin: 0;
  font-size: 24px;
  flex-grow: 1; /* Allows the heading to expand and push the button to the right */
  color: #ffffff;
}

.header button {
  background-color: transparent;
  color: #fff;
  border: none;
  cursor: pointer;
  font-size: 16px;
}

.header button:hover {
  text-decoration: underline;
}

Authentication - Sign Up and Sign in Setup
anchor

  1. Set Up Authentication:

    • We will use the Authenticator component from @aws-amplify/ui-react to manage the sign-in and sign-up processes.
    • We will also customize the authentication forms by defining formFields and signUpAttributes.
  2. Sign Up and Sign In:

    • By integrating the Authenticator component, users can sign up using their email, first name, and last name. AWS Amplify will manage the entire authentication workflow, including email verification.

Now, make the following changes to the App.js file.

App.js
import "./App.css";
import "@aws-amplify/ui-react/styles.css";
import {
  Button,
  Heading,
  Authenticator,
} from "@aws-amplify/ui-react";
import { signUp } from "@aws-amplify/auth";

const App = ({  }) => {

  const formFields = {
    signUp: {
      given_name: {
        placeholder: 'Enter your first name',
        isRequired: true,
        label: 'First Name'
      },
      family_name: {
        placeholder: 'Enter your last name',
        isRequired: true,
        label: 'Last Name'
      }
    },
  }
  const signUpAttributes = ["email", "given_name", "family_name"];

  return (
    <Authenticator
      formFields={formFields}
      signUpAttributes={signUpAttributes}
      /* Passing the addition wby_notes_app_group attribute to the signUp method
       * We use this attribute to assign the user to the notes-app-users group
       * The notes-app-users group has the necessary permissions to access the notes content in Webiny side
       */
      services={{
        async handleSignUp(formData) {
          const { options, username, password } = formData;
          options.userAttributes["custom:wby_notes_app_group"] = "notes-app-users";
          const res = await signUp({
            username,
            password,
            options,
          });
          return res;
        }
      }}
    >
      {({ signOut, user }) => (
        <main>
          <div className="header">
            <Heading level={1}>My Notes App! Welcome, {user.username}</Heading>
            <Button onClick={signOut} variation="outline">
              Sign out
            </Button>
          </div>
        </main>
      )}
    </Authenticator>
  );
};

export default App;

The sign-up and login forms are now ready. To test them, start your React app by running the npm start command.

GraphQL Setup: List Notes Query and Create & Delete Notes Mutations
anchor

We’ll be implementing the following Notes functionality:

  1. Add Notes
  2. Fetch Notes
  3. Delete Notes

Since we’ll be interacting with the Webiny CMS GraphQL API, we’ve already installed the Apollo Client package during the React project setup.

To get started, let’s create the GraphQL client, queries, and mutations for handling Notes operations.

First, create a graphql directory in the src folder, and then add a client.js file inside it.

The uri mentioned below is the Manage API URL for the Headless CMS. For more details on how to obtain the Manage API URL, refer to this guide.

src/graphql/client.js
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { fetchAuthSession } from 'aws-amplify/auth';

// Create an HTTP link
const httpLink = createHttpLink({
  // Headless CMS GraphQL Manage API End Point
  uri: 'https://XXXXXXXXX.cloudfront.net/cms/manage/en-US'
});

// Set up authentication context for the client
const authLink = setContext(async (_, { headers }) => {
  // Fetch the authentication session
  const { idToken } = (await fetchAuthSession()).tokens ?? {};

  // Return the headers with authentication token and additional headers
  return {
    headers: {
      ...headers,
      Authorization: idToken ? `Bearer ${idToken}` : '',
      'x-tenant': 'root'
    },
  };
});

// Create the Apollo Client instance
const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
});

export default client;

Next, let’s create a listNotes query to fetch all the notes.

To do this, add a queries.js file in the graphql directory.

graphql/queries.js
import { gql } from '@apollo/client';

export const listNotes = gql`
  query ListNotes {
    listNotes {
      data {
        id
        title
        description
      }
    }
  }
`;

Now, let’s add two mutations: one for creating notes and another for deleting them.

Create a mutations.js file in the graphql directory to define these mutations.

graphql/mutations.js
import { gql } from '@apollo/client';

export const createNote = gql`
  mutation CreateNote($data: NoteInput!) {
    createNote(data: $data) {
      data {
        id
        title
        description
      }
    }
  }
`;

export const deleteNote = gql`
  mutation DeleteNote(
    $revision: ID!, $options: CmsDeleteEntryOptions
  ) {
    deleteNote(revision: $revision, options: $options) {
      data
    }
  }
`;

Create Notes UI and Integrate GraphQL Queries & Mutations
anchor

Next, we’ll update the App.js file to build the interface for creating, listing, and deleting notes. We’ll also integrate the GraphQL queries and mutations we created earlier.

Replace the content of your App.js file with the following code snippet.

App.js
import React, { useState, useEffect } from "react";
import "./App.css";
import "@aws-amplify/ui-react/styles.css";
import {
  Button,
  Flex,
  Heading,
  Text,
  TextField,
  View,
  Authenticator,
} from "@aws-amplify/ui-react";
import { signUp } from "aws-amplify/auth";

import client from "./graphql/client";

import { listNotes } from "./graphql/queries";
import {
  createNote as createNoteMutation,
  deleteNote as deleteNoteMutation
} from "./graphql/mutations";

const App = ({  }) => {
  const [notes, setNotes] = useState([]);

  useEffect(() => {
    fetchNotes();
  }, []);

  async function fetchNotes() {
    try {
      console.log("Fetching notes...");
      const { data } = await client.query({
        query: listNotes,
        fetchPolicy: "network-only"
      });
      const notesFromAPI = data.listNotes.data;
      console.log("Fetched notes:", notesFromAPI);
      setNotes(notesFromAPI);
    } catch (error) {
      console.error("Error fetching notes:", error);
    }
  }

  async function createNote(event) {
    event.preventDefault();
    const form = new FormData(event.target);
    const data = {
      data: {
        title: form.get("title"),
        description: form.get("description"),
      },
    };
    try {
      await client.mutate({
        mutation: createNoteMutation,
        variables: data
      });
      await fetchNotes();
      event.target.reset();
    } catch (error) {
      console.error("Error creating note:", error);
    }
  }

  async function deleteNote({ id }) {
    const newNotes = notes.filter((note) => note.id !== id);
    setNotes(newNotes);
    await client.mutate({
      mutation: deleteNoteMutation,
      variables: { revision: id  }
    });
  }

  const formFields = {
    signUp: {
      given_name: {
        placeholder: 'Enter your first name',
        isRequired: true,
        label: 'First Name'
      },
      family_name: {
        placeholder: 'Enter your last name',
        isRequired: true,
        label: 'Last Name'
      }
    },
  }

  const signUpAttributes = ["email", "given_name", "family_name"];

  return (
    <Authenticator
      formFields={formFields}
      signUpAttributes={signUpAttributes}
      /* Passing the addition wby_notes_app_group attribute to the signUp method
       * We use this attribute to assign the user to the notes-app-users group
       * The notes-app-users group has the necessary permissions to access the notes content in Webiny side
       */
      services={{
        async handleSignUp(formData) {
          const { options, username, password } = formData;
          options.userAttributes["custom:wby_notes_app_group"] = "notes-app-users";
          const res = await signUp({
            username,
            password,
            options,
          });
          return res;
        },
      }}
    >
      {({ signOut, user }) => (
        <main>
          <div className="header">
            <Heading level={1}>My Notes App! Welcome, {user.username}</Heading>
            <Button onClick={signOut} variation="outline">
              Sign out
            </Button>
          </div>

          <Heading level={1}></Heading>
          <View as="form" margin="3rem 0" onSubmit={createNote}>
            <Flex direction="row" justifyContent="center">
              <TextField
                name="title"
                placeholder="Note Title"
                label="Note Title"
                labelHidden
                variation="quiet"
                required
              />
              <TextField
                name="description"
                placeholder="Note Description"
                label="Note Description"
                labelHidden
                variation="quiet"
                required
              />
              <Button type="submit" variation="primary">
                Create Note
              </Button>
            </Flex>
          </View>

          <Flex justifyContent="center">
            <Heading level={2}>Current Notes</Heading>
          </Flex>

          <View margin="3rem 0">
            {notes.map((note) => (
              <Flex
                key={note.id || note.title}
                direction="row"
                justifyContent="center"
                alignItems="center"
              >
                <Text as="strong" fontWeight={700}>
                  {note.title}
                </Text>
                <Text as="span">{note.description}</Text>
                <Button variation="link" onClick={() => deleteNote(note)}>
                  Delete note
                </Button>
              </Flex>
            ))}
          </View>
        </main>
      )}
    </Authenticator>
  );
};

export default App;

Testing the Application
anchor

We’re all set! Run the React app with the npm start command, and try signing up and logging in. You can also play with creating, viewing, and deleting notes.