Building a Crud application using Preact
What is Preact?
Preact is a Javascript library, although similar to React, but tiny. At the moment of writing, the main Preact library is around 4Kb. This is small enough that it’s possible to add React-like features to web pages in barely more code than is required to write native JavaScript.
Preact is not React. It is a separate library, but it is designed to be as close to React as possible while much smaller. Preact lets you choose how to use it: a small JavaScript library included in a web page (the no tools approach) or as a full-blown JavaScript application.
Why use Preact over React?
React applications can be large. There are times when you might wish to construct an app with React-like attributes, however, without needing to download a massive amount of JavaScript code.
If you want React-features but don’t want to pay the expense of a React-size JavaScript bundle, you might prefer to consider using Preact. However, it does have a downside. React’s virtual DOM requires a lot of code to keep it up to date. It needs to manage an entire synthetic event model, which parallels the one in the browser.
In this article, we will build a To-do app using Preact with Full Crud Functionality.
Live demo
Prerequisites
Sequel to the preceding, we should possess a basic knowledge of JavaScript and Preact to aid easy comprehension of the steps to be followed in this article.
If you are new to Preact, consider reading it’s documentation first.
Step 1 – Setting up the Preact project
Now let’s start by installing Preact CLI into our machine.
npm install -g preact-cli
We will Now use the Preact CLI to build a simple application. To do that open up your terminal and type the following:
preact create default PreactCrud
After installation, move into the folder using the cd testapp
and run the application by executing following command:
npm run dev
Your application will be live at localhost:8090
like below:
Let’s get started by adding typescript support, Add the following configuration to your tsconfig.json
to transpile JSX to Preact-compatible JavaScript:
// TypeScript < 4.1.1
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
//...
}
}
and Rename your .jsx
files to .tsx
for TypeScript to correctly parse your JSX.
Step 2 – Creating the required components
Let’s get started by creating directories, files and initializing the project.
to create the required components, execute the following commands in the terminal:
cd src/components
mkdir CrudItem
mkdir EditIem
cd crudItem
touch index.tsx
touch style.scss
cd..
cd EditItem
touch index.tsx
touch style.scss
Now open up CrudItem/index.tsx
and add the following code:
import { FunctionalComponent, h, Fragment } from "preact";
import DeleteIcon from "../icons/DeleteIcon";
import EditIcon from "../icons/EditIcon";
import * as style from "./style.scss";
type Props = {
text: string;
id: string;
isCompleted: boolean;
onEditTodo: (id: string) => void;
onDeleteTodo: (id: string) => void;
onToggleCompleted: (id: string) => void;
};
const TodoItem: FunctionalComponent<Props> = ({
text,
id,
isCompleted,
onEditTodo,
onDeleteTodo,
onToggleCompleted
}) => {
return (
<Fragment>
<div
onClick={() => onToggleCompleted(id)}
class={`${style.todoText} ${
isCompleted ? style.todoTextCompleted : ""
}`}
>
{text}
</div>
{!isCompleted && (
<div class={style.todoActions}>
<span onClick={() => onEditTodo(id)}>
<EditIcon />
</span>
<span onClick={() => onDeleteTodo(id)}>
<DeleteIcon />
</span>
</div>
)}
</Fragment>
);
};
export default TodoItem;
In the code snippet above, we are importing FunctionalComponent, h, & Fragment from Preact. Functional components are plain functions that receive props as the first argument, The code snippet above will be responsible for actions on our todo items.
update the CrudItem/style.scss
with the following:
.todoText {
width: 80%;
padding: 14px 20px 14px 20px;
}
.todoTextCompleted {
width: 100%;
text-decoration: line-through;
}
.todoActions {
width: 20%;
padding: 10px 20px 10px 20px;
display: flex;
justify-content: flex-end;
align-items: center;
& > *:not(:last-of-type) {
margin-right: 20px;
}
}
similarly open up EDitItem/index.tsx
and add the following code:
import { FunctionalComponent, h, JSX, Fragment } from "preact";
import { useState } from "preact/hooks";
import DiscardIcon from "../icons/DiscardIcon";
import SaveIcon from "../icons/SaveIcon";
import * as style from "./style.scss";
type Props = {
text: string;
id: string;
onSaveEditing: (id: string, value: string) => void;
onDiscardEditing: (id: string) => void;
};
const EditedTodoItem: FunctionalComponent<Props> = ({
text,
id,
onSaveEditing,
onDiscardEditing
}) => {
const [inputValue, setInputValue] = useState(text);
return (
<Fragment>
<input
onInput={e => setInputValue(e.currentTarget.value)}
class={style.todoItemInput}
type="text"
value={inputValue}
/>
<div class={style.todoActions}>
<span onClick={() => onSaveEditing(id, inputValue)}>
<SaveIcon />
</span>
<span onClick={() => onDiscardEditing(id)}>
<DiscardIcon />
</span>
</div>
</Fragment>
);
};
export default EditedTodoItem;
similarly, Here we are also receiving props as the first argument, after that, we are defining a variable EditedTodoItem
with the functional component which will be responsible for adding and editing to-do item.
update the EditItem/style.scss
with the following:
.todoActions {
padding: 10px 20px 10px 20px;
display: flex;
align-items: center;
& > *:not(:last-of-type) {
margin-right: 20px;
}
}
.todoItemInput {
padding: 13px 20px 10px 20px;
font-size: 18px;
color: #2980b9;
background-color: #f7f7f7;
width: 100%;
box-sizing: border-box;
border: 3px solid rgba(0, 0, 0, 0);
&:focus {
background: #fff;
border: 3px solid #2980b9;
outline: none;
}
}
Now update the src\components\header
file with the following code:
// this file is automatically created with the default template
import { FunctionalComponent, h } from "preact";
import { Link } from "preact-router/match";
import * as style from "./style.scss";
const Header: FunctionalComponent = () => {
return (
<header class={style.header}>
<h3>Codesource.io Tutorial - Preact TODO List app</h3>
<nav>
<Link activeClassName={style.active} href="/">
Homepage
</Link>
<Link activeClassName={style.active} href="/list">
My List
</Link>
</nav>
</header>
);
};
export default Header;
the code snippet above is the header for our Preact crud application. after that update the src\components\header\style.scss
with the following:
.header {
width: 100%;
padding: 0;
background: #4787ed;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
z-index: 50;
display: flex;
align-items: center;
justify-content: space-evenly;
& h3 {
text-align: center;
margin: 0;
color: white;
padding: 20px 0;
}
& nav {
& a {
text-transform: uppercase;
font-weight: 700;
font-size: 24px;
color: white;
text-decoration: none;
transition: 0.3s all ease-out;
&:not(:last-of-type) {
margin-right: 30px;
}
&:hover {
color: #fcf7f779;
text-decoration: underline;
}
}
}
}
.active {
text-decoration: underline !important;
}
Now open \src\components\app.tsx
and update the code with following:
import { FunctionalComponent, h } from "preact";
import { Route, Router, RouterOnChangeArgs } from "preact-router";
import Home from "../routes/home";
import ListPage from "../routes/ListPage";
import NotFoundPage from "../routes/notfound";
import TodosProvider from "./context";
import Header from "./header";
const App: FunctionalComponent = () => {
let currentUrl: string;
const handleRoute = (e: RouterOnChangeArgs) => {
currentUrl = e.url;
};
return (
<div id="app">
<TodosProvider>
<Header />
<Router onChange={handleRoute}>
<Route path="/" component={Home} />
<Route path="/list" component={ListPage} />
<NotFoundPage default />
</Router>
</TodosProvider>
</div>
);
};
export default App;
next, we will create a file .tsx
in \src\components
folder to add global state management in our app. execute the following command,
cd src/componenets
touch cd context.tsx
and add the following code to it:
import { createContext, FunctionalComponent, h } from "preact";
import { StateUpdater, useContext, useMemo, useState } from "preact/hooks";
import { TodoItem } from "../routes/home";
type TodosContextType = {
todos: TodoItem[];
setTodos: StateUpdater<TodoItem[]>;
onDeleteTodo: (id: string) => void;
onEditTodo: (id: string) => void;
onSaveEditing: (id: string, value: string) => void;
onDiscardEditing: (id: string) => void;
onToggleCompleted: (id: string) => void;
};
export const initialState: TodoItem[] = [
{
id: "1",
text: "Some todo 1",
isEditing: false,
isCompleted: false
},
{
id: "2",
text: "Some todo 2",
isEditing: false,
isCompleted: true
},
{
id: "3",
text: "Some todo 3",
isEditing: false,
isCompleted: false
}
];
export const TodosContext = createContext<TodosContextType>({
todos: [],
setTodos: () => {},
onDeleteTodo: () => {},
onEditTodo: () => {},
onSaveEditing: () => {},
onDiscardEditing: () => {},
onToggleCompleted: () => {}
});
const TodosProvider: FunctionalComponent = ({ children }) => {
const [todos, setTodos] = useState(initialState);
const onDeleteTodo = (id: string) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
const onEditTodo = (id: string) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id
? { ...todo, isEditing: !todo.isEditing }
: { ...todo, isEditing: false }
)
);
};
const onSaveEditing = (id: string, value: string) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, text: value, isEditing: false } : todo
)
);
};
const onDiscardEditing = (id: string) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, isEditing: !todo.isEditing } : todo
)
);
};
const onToggleCompleted = (id: string) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
)
);
};
const todosAPI = useMemo(() => {
return {
todos,
setTodos,
onDeleteTodo,
onEditTodo,
onSaveEditing,
onDiscardEditing,
onToggleCompleted
};
}, [todos, setTodos]);
return (
<TodosContext.Provider value={todosAPI}>{children}</TodosContext.Provider>
);
};
export const useTodos = () => useContext(TodosContext);
export default TodosProvider;
As we can see in the code snippet above we have imported CreateContext
method which allows us to pass a value to a child deep down the tree without having to pass it through every component in-between via props.
Step 3 – Working with Routes
to create the required Routes, execute the following commands in terminal:
cd src/routes/
mkdir ListPage
cd ListPage
touch index.tsx
touch style.scss
Now open up Listpage/index.tsx
and add the following code:
import { Fragment, h } from "preact";
import { useTodos } from "../../components/context";
import * as style from "./style.scss";
const ListPage = () => {
const { todos, onDeleteTodo } = useTodos();
console.log("todos", todos);
return (
<div class={style.listPage}>
<h2>list page</h2>
<p style={{ marginTop: "100px" }}>
{todos.map(({ text, id }) => (
<Fragment>
<p class={style.listItem}>
{text}
<button onClick={() => onDeleteTodo(id)}>Delete this TODO</button>
</p>
<hr />
</Fragment>
))}
</p>
</div>
);
};
export default ListPage;
the default theme comes with Preact router preloaded you can learn more about Preact Router here.
update the ListPage/style.scss
with the following:
.listPage {
width: 600px;
margin: 100px auto;
}
.listItem {
display: flex;
justify-content: space-between;
}
update the routes\home\index.tsx
with the following:
// this file is shipped with Preact default template
import { FunctionalComponent, h, JSX } from "preact";
import { useEffect, useState } from "preact/hooks";
import { useTodos } from "../../components/context";
import EditedTodoItem from "../../components/EditItem";
import TodoItem from "../../components/CrudItem";
import * as style from "./style.scss";
interface Props {
user: string;
}
export type TodoItem = {
id: string;
text: string;
isEditing: boolean;
isCompleted: boolean;
};
type Filters = "all" | "done" | "undone";
const Home: FunctionalComponent<Props> = () => {
const {
todos,
setTodos,
onDeleteTodo,
onEditTodo,
onSaveEditing,
onDiscardEditing,
onToggleCompleted
} = useTodos();
const [todoInSearch, setTodoInSearch] = useState<string>("");
const [todosToShow, setTodosToShow] = useState<TodoItem[]>(todos);
const [activeFilter, setActiveFilter] = useState<Filters>("all");
useEffect(() => {
switch (activeFilter) {
case "all":
setTodosToShow(todos);
break;
case "done":
setTodosToShow(todos.filter(todo => Boolean(todo.isCompleted)));
break;
case "undone":
setTodosToShow(todos.filter(todo => todo.isCompleted === false));
break;
}
}, [activeFilter, todos]);
const onInputNewTodo = ({
currentTarget
}: JSX.TargetedEvent<HTMLInputElement, Event>) => {
setTodoInSearch(currentTarget.value);
};
const onAddNewTodo = () => {
setTodos(prev => [
{
id: String(Date.now()),
text: todoInSearch,
isEditing: false,
isCompleted: false
},
...prev
]);
setTodoInSearch("");
setActiveFilter("all");
};
const isActive = (filter: Filters) =>
activeFilter === filter ? style.active : "";
return (
<div class={style.todo}>
<div class={style.addInputWrapper}>
<input
placeholder="Add new todo ←"
value={todoInSearch}
onInput={onInputNewTodo}
type="text"
/>
<button onClick={onAddNewTodo} disabled={!todoInSearch}>
Add todo
</button>
</div>
<div class={style.filterWrapper}>
<button class={isActive("all")} onClick={() => setActiveFilter("all")}>
All
</button>
<button
class={isActive("done")}
onClick={() => setActiveFilter("done")}
>
Done
</button>
<button
class={isActive("undone")}
onClick={() => setActiveFilter("undone")}
>
Undone
</button>
</div>
<ul class={style.todoList}>
{todosToShow.map(({ id, text, isEditing, isCompleted }) => (
<li
class={`${style.todoListItem} ${
isCompleted ? style.todoListItemCompleted : ""
}`}
>
{isEditing ? (
<EditedTodoItem
id={id}
text={text}
onSaveEditing={onSaveEditing}
onDiscardEditing={onDiscardEditing}
/>
) : (
<TodoItem
onToggleCompleted={onToggleCompleted}
isCompleted={isCompleted}
id={id}
text={text}
onDeleteTodo={onDeleteTodo}
onEditTodo={onEditTodo}
/>
)}
</li>
))}
</ul>
</div>
);
};
export default Home;
and update routes\home\style.scss
with the following:
.todo {
width: 600px;
margin: 100px auto;
box-shadow: 0 8px 8px rgba(0, 0, 0, 0.3);
}
.addInputWrapper {
display: flex;
& input {
font-size: 18px;
color: #2980b9;
background-color: #f7f7f7;
width: 100%;
padding: 13px 13px 13px 20px;
box-sizing: border-box;
border: 3px solid rgba(0, 0, 0, 0);
width: 80%;
&:focus {
background: #fff;
border: 3px solid #2980b9;
outline: none;
}
}
& button {
width: 20%;
font-size: 16px;
text-transform: uppercase;
transition: 0.3s all ease-out;
&:enabled {
background: rgb(66, 199, 66);
border: none;
font-weight: 700;
color: white;
}
}
}
.filterWrapper {
display: flex;
margin: 0;
padding: 0;
& > button {
cursor: pointer;
width: 33.3%;
text-align: center;
border: 1px solid #cacaca;
font-weight: 700;
padding: 10px;
font-size: 16px;
transition: 0.3s hover ease-out;
&.active {
background: rgb(202, 201, 201) !important;
}
&:hover {
background: rgb(226, 226, 226);
}
}
}
.todoList {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.todoListItem {
position: relative;
background: #51f51f79;
color: #666;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 20px;
font-weight: normal;
transition: all 1s;
word-break: break-all;
&:nth-child(even) {
background: #e78c8c79;
}
& span {
height: 30px;
}
& svg {
height: 100%;
&:hover {
cursor: pointer;
& path {
fill: red;
}
}
}
}
.todoListItemCompleted {
background: rgb(180, 180, 180) !important;
}
Step – 4 Run build the app
the size of the Preact application in dev mode is over 300Kb. That’s pretty large, To see the real power of Preact, stop the dev server and then run the build
script:
npm run build
Conclusion
Preact is a remarkable project. Despite working in a very different way to React, it provides virtually the same power, at a fraction of the size. And the fact that it can be used for anything between the lowliest inline code to a full-blown SPA means it is well worth considering if code-size is critical to your project.
I hope you learned a few things about Preact. Every article can be made better, so please leave your suggestions and contributions in the comments below. If you have questions about any of the steps, please do also ask in the comments section below.
You can Checkout Source Code on Github