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.