preact crud

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


Share on social media

//