Building a CRUD Mobile App with Ionic Framework and React

Photo by Francesco on Unsplash

Building a CRUD Mobile App with Ionic Framework and React

Ionic and React are two popular frameworks for building mobile apps. Ionic is a UI framework that provides a consistent look and feels across different platforms, while React is a JavaScript framework that makes it easy to build complex user interfaces.

In this article, we will show you how to build a mobile app for Android using Ionic and React. We will start by creating a new project and then we will add some basic components to the app. And finally using a emulator to simulate how the app will work on a Android device.

Ionic

Ionic is an open-source UI toolkit for building mobile apps using web technologies: HTML, CSS, and Javascript. It offers integration for React, Vue and Angular.

Requirements

  • NodeJS installed.

  • React knowledge.

  • Android Studio, download it here.

  • Android SDK installed (Instructions here)

Installation

We need to install the Ionic CLI tool. Ionic apps are created and developed primarily through the Ionic command-line utility. The Ionic CLI is the preferred method of installation, as it offers a wide range of dev tools and help options along the way.

Install the Ionic CLI with npm:

 npm install -g @ionic/cli

Let's set up our project using the ionic start command:

ionic start my-crud-app blank --type=react --capacitor

After the CLI installed all the dependencies, we run ionic serve to start the app on our browser.

We go to localhost:8100, we should be able to see this page:

Adding a Task

We create a new folder, src/components. And a new file src/components/Task.tsx .

In this file, we are going to write all the code to perform CRUD operations.

import React, { useState, useEffect } from "react";
import {
  IonApp,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonContent,
  IonList,
  IonItem,
  IonLabel,
  IonInput,
  IonCheckbox,
  IonButton,
  IonIcon,
  InputChangeEventDetail
} from "@ionic/react";
import { add, trash, create, checkmark } from "ionicons/icons";

type Task = {
  id: number;
  task: string;
  completed: boolean;
};

const Tasks: React.FC = () => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [newTask, setNewTask] = useState<string>("");

  useEffect(() => {
    const savedTasks = localStorage.getItem("tasks");
    if (savedTasks) {
      setTasks(JSON.parse(savedTasks));
    }
  }, []);

  useEffect(() => {
    localStorage.setItem("tasks", JSON.stringify(tasks));
  }, [tasks]);

  const handleTaskInput = (event: CustomEvent<InputChangeEventDetail>) => {
    setNewTask((event.target as HTMLInputElement).value);
  };

  const handleNewTask = () => {
    const task: Task = {
      id: Date.now(),
      task: newTask,
      completed: false
    };
    setTasks([...tasks, task]);
    setNewTask("");
  };

  return (
    <IonApp>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Ionic React CRUD</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList>
          {tasks.map((task) => (
            <IonItem key={task.id}>
              <IonLabel>
                <h2>{task.task}</h2>
              </IonLabel>
              <div slot="end">
              </div>
            </IonItem>
          ))}
        </IonList>
        <IonItem>
          <IonInput
            placeholder="Add Task"
            value={newTask}
            onIonChange={handleTaskInput}
          />
          <IonButton color="success" onClick={handleNewTask}>
            <IonIcon icon={add} />
            <IonLabel>Add</IonLabel>
          </IonButton>
        </IonItem>
      </IonContent>
    </IonApp>
  );
};

export default Tasks;

Here we define a component called "Tasks" which lists tasks and allows the user to add new ones.

Upon import, the code first sets up local state variables for tasks and newTask using the useState hook. It then sets up two useEffect hooks. The first useEffect hook retrieves the tasks from local storage upon the component mount, if present. The second useEffect hook saves tasks to local storage upon changes to the tasks state.

The handleTaskInput function sets the new task value as the user types into the input field, and the handleNewTask function creates a new task object, adds it to the tasks array, and resets the input field. The return statement includes the Ionic components which display the task list and input form.

App.tsx

Now we go to src/App.tsx to create a route to the component we just created.

import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import Tasks from './components/Tasks';


/* 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';


setupIonicReact();

const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
        <Route path="/" component={Tasks} exact={true} />

      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
);

export default App;

The main component is defined as App and uses a functional component with no props (React.FC = () =>). Inside the component, there's a <IonReactRouter> component that enables React Router for the app, and inside it there is an <IonRouterOutlet> component, which serves as the container for rendering child components based on the current route.

The <Route> component is used to define each route and its corresponding component. Here, the "/" route is defined with the Tasks component as the component to render when the route matches. "exact" attribute is set to true to only match the "/" route exactly instead of matching any route that starts with "/" (such as "/about" or "/contact").

If we go to our browser an visit localhost:8100, we will have this view:

Update a Task

const Tasks: React.FC = () => {
  ...
  const handleTaskEdit = (id: number, taskText: string) => {
    const updatedTasks = tasks.map((task) => {
      if (task.id === id) {
        return { ...task, task: taskText };
      }
      return task;
    });
    setTasks(updatedTasks);
  };

  const handleTaskToggle = (id: number) => {
    const updatedTasks = tasks.map((task) => {
      if (task.id === id) {
        return { ...task, completed: !task.completed };
      }
      return task;
    });
    setTasks(updatedTasks);
  };

  return (
   ...
  );
};

export default Tasks;

This code defines two event handler functions handleTaskEdit and handleTaskToggle, which update the state of tasks in a react component.

handleTaskEdit takes two parameters: id of type number and taskText of type string. It uses the map method on the tasks array to find the task with the matching id and return a new object that combines the existing task object with the updated taskText. The new array of tasks is then set as the state of the component using the setTasks function from the React state hook.

handleTaskToggle takes a single parameter id of type number. It also uses the map method on the tasks array to find the task object with the matching id and return a new object that combines the existing task object with the completed property toggled to the opposite boolean value. The new array of tasks is then set as the state of the component using the setTasks function.

 return (
    <IonApp>
      ...
      <IonContent>
        <IonList>
          ...
                <IonCheckbox
                  checked={task.completed}
                  onIonChange={() => handleTaskToggle(task.id)}
                />
              </IonLabel>
              <div slot="end">
                <IonButton
                  color="primary"
                  onClick={() =>
                    handleTaskEdit(
                      task.id,
                      window.prompt("Update Task", task.task) || ""
                    )
                  }
                >
                  <IonIcon icon={create} />
                </IonButton>
              </div>
            </IonItem>
          ))}
        </IonList>
        ...
      </IonContent>
    </IonApp>
  );

The checked prop of the IonCheckbox is set to the completed property of each task object, and the onIonChange prop calls the handleTaskToggle function with the task's id when the checkbox state changes.

The IonButton component is used to set up an edit button. When the button is clicked, the handleTaskEdit function is called with the task's id and a prompt to update the task text.

Delete a Task

const handleTaskDelete = (id: number) => {
    const updatedTasks = tasks.filter((task) => task.id !== id);
    setTasks(updatedTasks);
  };

Here we define a function called handleTaskDelete which takes a single argument id of type number.

Inside the function, it uses the filter() method to create a new array called updatedTasks that contains all tasks except for the one with the id passed to the function.

Finally, it calls the setTasks function with updatedTasks as the argument. This function is likely part of a React component, and calling it will trigger a re-render of the component with the updated task list. The end result is that this function removes a task from the tasks list and triggers a re-render to reflect the updated list in the UI.

 return (
    <IonApp>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Ionic React CRUD</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList>
              ...
              <div slot="end">
                <IonButton
                  color="danger"
                  onClick={() => handleTaskDelete(task.id)}
                >
                  <IonIcon icon={trash} />
                </IonButton>
                ...
              </div>
            </IonItem>
          ))}
        </IonList>
        ..
      </IonContent>
    </IonApp>
  );

The content of the app is contained within the IonContent component, which contains an IonList component. In the IonList, there can be multiple IonItem components that represent individual tasks.

Within each IonItem, there is a div with a slot attribute of "end". Within this div, there is an IonButton component that has a color attribute set to "danger" to represent a delete action.

When the button is clicked, it triggers the handleTaskDelete function (which we defined earlier and is likely declared in the same file) with the id of the task as an argument. This removes the task from the tasks list and triggers a re-render of the component to reflect the updated list.

Complete component code

import React, { useState, useEffect } from "react";
import {
  IonApp,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonContent,
  IonList,
  IonItem,
  IonLabel,
  IonInput,
  IonCheckbox,
  IonButton,
  IonIcon,
  InputChangeEventDetail
} from "@ionic/react";
import { add, trash, create, checkmark } from "ionicons/icons";

type Task = {
  id: number;
  task: string;
  completed: boolean;
};

const Tasks: React.FC = () => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [newTask, setNewTask] = useState<string>("");

  useEffect(() => {
    const savedTasks = localStorage.getItem("tasks");
    if (savedTasks) {
      setTasks(JSON.parse(savedTasks));
    }
  }, []);

  useEffect(() => {
    localStorage.setItem("tasks", JSON.stringify(tasks));
  }, [tasks]);

  const handleTaskInput = (event: CustomEvent<InputChangeEventDetail>) => {
    setNewTask((event.target as HTMLInputElement).value);
  };

  const handleNewTask = () => {
    const task: Task = {
      id: Date.now(),
      task: newTask,
      completed: false
    };
    setTasks([...tasks, task]);
    setNewTask("");
  };

  const handleTaskDelete = (id: number) => {
    const updatedTasks = tasks.filter((task) => task.id !== id);
    setTasks(updatedTasks);
  };

  const handleTaskEdit = (id: number, taskText: string) => {
    const updatedTasks = tasks.map((task) => {
      if (task.id === id) {
        return { ...task, task: taskText };
      }
      return task;
    });
    setTasks(updatedTasks);
  };

  const handleTaskToggle = (id: number) => {
    const updatedTasks = tasks.map((task) => {
      if (task.id === id) {
        return { ...task, completed: !task.completed };
      }
      return task;
    });
    setTasks(updatedTasks);
  };

  return (
    <IonApp>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Ionic React CRUD</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList>
          {tasks.map((task) => (
            <IonItem key={task.id}>
              <IonLabel>
                <h2>{task.task}</h2>
                <IonCheckbox
                  checked={task.completed}
                  onIonChange={() => handleTaskToggle(task.id)}
                />
              </IonLabel>
              <div slot="end">
                <IonButton
                  color="danger"
                  onClick={() => handleTaskDelete(task.id)}
                >
                  <IonIcon icon={trash} />
                </IonButton>
                <IonButton
                  color="primary"
                  onClick={() =>
                    handleTaskEdit(
                      task.id,
                      window.prompt("Update Task", task.task) || ""
                    )
                  }
                >
                  <IonIcon icon={create} />
                </IonButton>
              </div>
            </IonItem>
          ))}
        </IonList>
        <IonItem>
          <IonInput
            placeholder="Add Task"
            value={newTask}
            onIonChange={handleTaskInput}
          />
          <IonButton color="success" onClick={handleNewTask}>
            <IonIcon icon={add} />
            <IonLabel>Add</IonLabel>
          </IonButton>
        </IonItem>
      </IonContent>
    </IonApp>
  );
};

export default Tasks;

Running the app in the Android emulator

To run the app in an Android emulator, we need to have installed the Android SDK.

We run the following command in our command line:

ionic capacitor run android

Ionic will ask you the device target. This message will not appear if we don't have the Android SDK installed.

Then, an Android device will appear on your screen.

But sometimes it can appear an error message like this:

In that case, what works for me was to open the project with Android Studio. And the IDE will install some Gradle files. After the installation is completed. We can run again the ionic capacitor run android command or click on the Run button in Android Studio.

Conclusion

If you are interested in building mobile apps, then Ionic and React are great choices. With these frameworks, you can create beautiful, responsive apps that can be used on any device. Building with Ionic offers some benefits like cross-platform compatibility, so we can build apps that run on both iOS and Android devices. Ionic components are reusable, so we can use them in multiple apps.

Thank you for taking the time to read this article.

If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through Twitter, or LinkedIn.

The source code is here.

Resources