Ionic React JS AWS Amplify Authentication CRUD Tutorial Using Datastore & Storage API

Aaron K Saunders - Jun 4 '21 - - Dev Community

What We Are Building

mobile application image

Videos

  • Ionic React JS AWS Amplify Authentication CRUD Tutorial Part 1, Authentication UI Component: Integrate AWS Amplify with Ionic React JS - this is part one of a series of video about using AWS Amplify with React JS and Ionic Framework. In the series, we will utilize the new Amplify Admin UI, Create Models and some relationships and then finally deploy the application to Amazon for hosting.

  • Ionic React JS AWS Amplify Authentication CRUD Tutorial Part 2, Authentication UI Component: Integrate AWS Amplify with Ionic React JS - this is part one of a series of video about using AWS Amplify with React JS and Ionic Framework. We will utilize the new Amplify Admin UI, Create Models and some relationships and then finally deploy the application to Amazon for hosting.

  • Ionic React JS AWS Amplify Authentication CRUD Tutorial Part 3, Working With Storage API and Images: We cover a lot in this video of using AWS Amplify Storage and Datastore APIs with Ionic Framework and React JS.

We are associating an image with each Task object. The image is stored in an S3Bucket using the AWS Amplify Storage API. We also show one approach for changing the image associated with the Task object or removing the image completely from the Task Object.

Final Source Code

// src/App.tsx
import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import Home from './pages/Home';

/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';

/* Theme variables */
import './theme/variables.css';

const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
        <Route exact path="/home">
          <Home />
        </Route>
        <Route exact path="/">
          <Redirect to="/home" />
        </Route>
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
);

export default App;
Enter fullscreen mode Exit fullscreen mode
// src/pages/Home.tsx
import {
  IonButton,
  IonButtons,
  IonContent,
  IonFooter,
  IonHeader,
  IonModal,
  IonPage,
  IonTitle,
  IonToolbar,
} from "@ionic/react";
import "./Home.css";

import Amplify, { DataStore, Storage } from "aws-amplify";
import { AmplifyAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react";
import awsconfig from "../aws-exports";
import React, { useState } from "react";
import ListTasks from "../components/ListTasks";
import EntryForm from "../components/EntryForm";
import { Task } from "../models";
import { create } from "domain";

Amplify.configure(awsconfig);

const Home: React.FC = () => {
  const [showModal, setShowModal] = useState({ isOpen: false, data: null });

  /**
   *
   * @param value
   * @returns
   */
  const updateTask = async (value: any) => {
    // update
    const original = await DataStore.query(Task, value.id);
    if (original === undefined) return;

    // check to see if there is a new file
    let fileSaveResp: any = null;

    // save original imageKey
    let newImageKey: any = original.imageKey;

    if (value?.file?.lastModified) {
      // saving new image
      fileSaveResp = await Storage.put(
        encodeURIComponent(value?.file.name),
        value?.file
      );

      // delete old image
      if (original.imageKey) await Storage.remove(original.imageKey);

      // set new imageKey
      newImageKey = fileSaveResp?.key;
    }
    // User Cleared Out The EXISTING Image
    else if (original.imageKey && !value?.file) {
      // delete the old image, dont add anything
      if (original.imageKey) await Storage.remove(original.imageKey);

      // clear the imageKey since there is no file
      newImageKey = null;
    }

    return await DataStore.save(
      Task.copyOf(original, (updated: any) => {
        updated.title = value.title;
        updated.description = value.description;
        updated.imageKey = newImageKey
      })
    );
  };

  /**
   *
   * @param value
   */
  const createTask = async (value: any) => {
    // create
    let fileResp: any = null;

    if (value?.file) {
      fileResp = await Storage.put(
        encodeURIComponent(value?.file.name),
        value?.file
      );
    }
    return await DataStore.save(
      new Task({
        title: value?.title,
        description: value?.description,
        imageKey: fileResp?.key,
      })
    );
  };

  /**
   *
   * @param value
   */
  const handleCloseModal = async (value: any) => {
    console.log(value);

    if (value) {
      if (value?.id) {
        await updateTask(value);
      } else {
        await createTask(value);
      }
    }
    setShowModal({ isOpen: false, data: null });
  };

  /**
   *
   * @param value
   */
  const editData = (value: any) => {
    setShowModal({ isOpen: true, data: value });
  };

  /**
   *
   * @param value
   */
  const deleteData = async (value: any) => {
    await DataStore.delete(Task, (t: any) => t.id("eq", value.id));
  };

  return (
    <AmplifyAuthenticator usernameAlias="email">
      <IonPage>
        <IonHeader>
          <IonToolbar>
            <IonTitle>Ionic React AWS Amplify</IonTitle>
            <IonButtons slot="end">
              <IonButton
                onClick={() => setShowModal({ isOpen: true, data: null })}
              >
                NEW
              </IonButton>
            </IonButtons>
          </IonToolbar>
        </IonHeader>
        <IonContent fullscreen>
          {/* <!-- MODAL --> */}

          <IonModal isOpen={showModal.isOpen}>
            <EntryForm
              onCloseModal={handleCloseModal}
              initialValues={showModal.data}
            />
          </IonModal>
          <ListTasks onDelete={deleteData} onEdit={editData} />
        </IonContent>
        <IonFooter>
          <AmplifySignOut />
        </IonFooter>
      </IonPage>
    </AmplifyAuthenticator>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode
// src/components/EntryForm.tsx
import React, { useEffect, useRef, useState } from "react";
import {
  IonButton,
  IonButtons,
  IonContent,
  IonFooter,
  IonHeader,
  IonPage,
  IonTitle,
  IonToolbar,
  IonInput,
  IonItem,
  IonLabel,
  IonTextarea,
} from "@ionic/react";
export interface EntryFormProps {
  onCloseModal: any;
  initialValues: any;
}

const EntryForm: React.FC<EntryFormProps> = ({
  onCloseModal,
  initialValues,
}) => {
  const titleRef = useRef<any>("");
  const descriptionRef = useRef<any>("");
  const fileRef = useRef<any>(null);

  const [currentFile, setCurrentFile] = useState<any>(null);

  useEffect(() => {
    titleRef.current.value = initialValues?.title;
    descriptionRef.current.value = initialValues?.description;

    if (initialValues?.imageKey) {
      const name = decodeURIComponent(initialValues?.imageKey);
      setCurrentFile({ name });
    }
  }, [initialValues]);

  /**
   *
   * @param e
   */
  const handleSave = (e: any) => {
    e.preventDefault();

    onCloseModal({
      title: titleRef.current?.value,
      description: descriptionRef.current?.value,
      file: currentFile,
      id: initialValues?.id || null,
    });
  };

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>ENTRY FORM</IonTitle>
          <IonButtons slot="end"></IonButtons>
        </IonToolbar>
      </IonHeader>
      <IonContent fullscreen>
        <IonItem>
          <IonLabel>Title</IonLabel>
          <IonInput ref={titleRef as any}></IonInput>
        </IonItem>

        <IonItem>
          <IonLabel>Description</IonLabel>
          <IonTextarea ref={descriptionRef as any} rows={3}></IonTextarea>
        </IonItem>

        {/* <!-- get file --> */}
        <IonItem>
          <IonLabel>
            <p>{currentFile ? currentFile?.name : null}</p>
          </IonLabel>
          <input
            ref={fileRef as any}
            type="file"
            style={{ display: "none" }}
            onChange={(e) =>
              setCurrentFile(e?.target?.files ? e?.target?.files[0] : null)
            }
          />
          {currentFile ? (
            <IonButton onClick={() => setCurrentFile(null)} color="danger">
              Clear
            </IonButton>
          ) : (
            <IonButton onClick={() => fileRef.current.click()}>
              Select File
            </IonButton>
          )}
        </IonItem>
      </IonContent>
      <IonFooter>
        <IonToolbar>
          <IonButton type="button" onClick={() => onCloseModal(null)}>
            CANCEL
          </IonButton>
          <IonButton onClick={handleSave}>SAVE</IonButton>
        </IonToolbar>
      </IonFooter>
    </IonPage>
  );
};

export default EntryForm;
Enter fullscreen mode Exit fullscreen mode
// src/components/ListTasks.tsx
import {
  IonButton,
  IonLabel,
  IonList,
  IonCard,
  IonCardContent,
} from "@ionic/react";
import { DataStore, Storage } from "aws-amplify";
import React, { useEffect, useState } from "react";
import { Task } from "../models";

export interface ListTasksProps {
  onEdit: any;
  onDelete: any;
}
const ListTasks: React.FC<ListTasksProps> = ({ onDelete, onEdit }) => {
  const [data, setData] = useState([] as any);
  useEffect(() => {
    const loadData = async () => {
      const tasks = await DataStore.query(Task);
      setData(tasks);
    };

    loadData();

    // data changes
    const resp = DataStore.observe(Task).subscribe(() => {
      loadData();
    });

    return () => resp.unsubscribe();
  }, []);

  return (
    <IonList>
      {data.map((t: any) => (
        <IonCard key={t.id}>
          <IonCardContent>
            <div style={{ width: "auto" }}>
              <ImageRender imageKey={t?.imageKey}></ImageRender>
            </div>
            <IonLabel class="ion-text-wrap">
              <p>{t.title}</p>
              <p>{t.description}</p>
              <p style={{ zoom: 0.8 }}>{t.id}</p>
              <div
                className="ion-float-right ion-padding"
                style={{ paddingRight: "0" }}
              >
                <IonButton style={{ zoom: 0.8 }} onClick={() => onEdit(t)}>
                  EDIT
                </IonButton>
                <IonButton
                  style={{ zoom: 0.8 }}
                  onClick={() => onDelete(t)}
                  color="danger"
                >
                  DELETE
                </IonButton>
              </div>
            </IonLabel>
          </IonCardContent>
        </IonCard>
      ))}
    </IonList>
  );
};

export default ListTasks;

export interface ImageRenderProps {
  imageKey: string;
}

const ImageRender: React.FC<ImageRenderProps> = ({ imageKey }) => {
  const [imageURL, setImageURL] = useState<any>("");

  useEffect(() => {

    try {
    imageKey &&
      Storage.get(imageKey, { download: true }).then((result: any) => {
        setImageURL(URL.createObjectURL(result.Body));
      });
    } catch (e) {
      setImageURL(null);
    }
  }, [imageKey]);
  return imageKey && imageURL ? <img src={imageURL} alt={imageKey} /> : null;
};
Enter fullscreen mode Exit fullscreen mode

Important Links

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .