Todo app with no client-side JavaScript using @lazarv/react-server
Tags: react, ssr, reactserver, vite
Published: 2024-05-27 17:02:41

Everyone starts with a simple Todo app when evaluating a new framework. So let’s do it this time using [@lazarv/react-server](https://react-server.dev), a new minimalist React meta-framework using Vite!

Our goal with this example is to use no client-side JavaScript and no React hydration. We only want to use React Server Components and Server Actions. Is this possible? Absolutely!

## Project setup

Let’s create a new project, by creating a new folder, initializing pnpm and installing all the required dependencies.

```bash
mkdir todo
cd todo
pnpm init
pnpm config set auto-install-peers true --location project
pnpm add @lazarv/react-server better-sqlite3 zod
pnpm add -D @types/better-sqlite3 @types/react @types/react-dom autoprefixer postcss tailwindcss typescript
pnpx tailwindcss init -p
```

To store our Todo items, we will use a local Sqlite database. For validation, we will use Zod and for styling we will use Tailwind CSS. To include all our source code as Tailwind content, change the **tailwind.config.js** to this:

```jsx
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.tsx"],
  theme: {
    extend: {},
  },
  plugins: [],
};
```

As we will not do any exciting Tailwind styling, put the usual 3-liner Tailwind setup into **src/index.css**:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

## Hello World!

Well it’s nothing better than a good old “Hello World!” app, so let’s create an entrypoint for our Todo app! Put the following code into **src/index.tsx**:

```jsx
export default function Index() {
  return (
	  <h1>Hello World!</h1>
  );
}
```

To run this micro-app, you can just use `pnpm exec react-server ./src/index.tsx` . To make our life easier, let’s add some npm scripts to **package.json**:

```jsx
"scripts": {
  "dev": "react-server ./src/index.tsx",
  "build": "react-server build ./src/index.tsx",
  "start": "react-server start"
},
```

After doing this, use `pnpm dev` to start the development server. After our development server is running, open [http://localhost:3000](http://localhost:3000) and be proud of our Hello World! app. You can also use `pnpm dev --open` to do so.

## Layout

Our Todo app needs a layout, so let’s create **src/Layout.tsx**:

```jsx
export default function Layout({ children }: React.PropsWithChildren) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Todo</title>
      </head>
      <body>
        <div className="p-4">
          <h1 className="text-4xl font-bold mb-4">
            <a href="/">Todo</a>
          </h1>
          {children}
        </div>
      </body>
    </html>
  );
}
```

Nothing fancy here, just a usual HTML document template. We will use this Layout component as a wrapper for our app.

## Page

Our main page will be the Todo app, where we will use all of the building blocks to create our app. Let’s say goodbye to Hello World! and change the component to this:

```jsx
import "./index.css";

import { allTodos } from "./actions";
import AddTodo from "./AddTodo";
import Item from "./Item";
import Layout from "./Layout";

export default function Index() {
  const todos = allTodos();

  return (
    <Layout>
      <AddTodo />
      {todos.length === 0 && <p className="text-gray-500">No todos yet!</p>}
      {todos.map((todo) => (
        <Item key={todo.id} title={todo.title} id={todo.id} />
      ))}
    </Layout>
  );
}
```

In this React Server Component, we collect all the stored Todo items by calling `allTodos()` and use the result to render JSX. We use the Layout component to wrap our content into a HTML document.

## Item

To render our items, let’s create an Item component in **src/Item.tsx**:

```jsx
import { deleteTodo } from "./actions";

type Props = {
  id: number;
  title: string;
};
export default function Item({ id, title }: Props) {
  return (
    <div className="flex row items-center justify-between py-1 px-4 my-1 rounded-lg text-lg border bg-gray-100 text-gray-600 mb-2">
      <p className="flex-1">{title}</p>
      <form action={deleteTodo}>
        <input type="hidden" name="id" value={id} />
        <button className="font-medium">Delete</button>
      </form>
    </div>
  );
}
```

The Item component will render our Todo item using an `id` and `title` prop. But what about that `<form action={deleteTodo}>`? It’s a server action! When the user will submit the form by clicking on the “Delete” button, the browser will call our server action. This is possible without any JavaScript on the frontend, as React supports progressive enhancement for server actions and the initial form action will call the server action by including a hidden input field in the form:

```jsx
<input type="hidden" name="$ACTION_ID_/Users/lazarv/Projects/tutorials/todo/src/actions.ts#deleteTodo">
```

The framework will resolve this `$ACTION_ID_` prefixed path to the server action and it will call our server action function!

## Server actions

We will use server actions to implement all functionality of our Todo app. This is the most complex part of the app, but don’t shy away, it’s still really very simple, let’s create **src/actions.ts**:

```jsx
"use server";

import { redirect } from "@lazarv/react-server";
import Database from "better-sqlite3";
import * as zod from "zod";

const db = new Database("db.sqlite");
db.exec(
  "CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT)"
);

type Todo = {
  id: number;
  title: string;
};

const addTodoSchema = zod.object({
  title: zod
    .string()
    .min(3, "Title must be at least 3 characters")
    .max(100, "Title must be at most 100 characters")
    .refine((value) => value.length > 0, "Title is required")
    .transform((value) => value.trim()),
});

const deleteTodoSchema = zod.object({
  id: zod.string().transform((value) => parseInt(value.trim(), 10)),
});

export async function addTodo(formData: FormData) {
  const result = addTodoSchema.safeParse(Object.fromEntries(formData));

  if (!result.success) {
    throw result.error.issues;
  }

  const { title } = result.data;
  db.prepare("INSERT INTO todos(title) VALUES (?)").run(title);
  redirect("/");
}

export function allTodos() {
  return db.prepare("SELECT * FROM todos").all() as Todo[];
}

export async function deleteTodo(formData: FormData) {
  const result = deleteTodoSchema.safeParse(Object.fromEntries(formData));

  if (!result.success) {
    throw result.error.issues;
  }

  const { id } = result.data;
  db.prepare("DELETE FROM todos WHERE id = ?").run(id);
  redirect("/");
}
```

In the first line of this file, we instrument the framework to treat this file as a server action module using the `“use server”;` directive. All exported async functions will be available for us to use as server actions.

We initialize the Sqlite database on module import and create Zod schemas for item add and delete operations.

In all server actions, you will receive a `FormData` instance, including all the fields we define in the forms. We `safeParse` these after converting to JavaScript objects using `Object.fromEntries`.

If Zod validation fails, we throw the validation issues as an error.

On success, we run a database command to INSERT or DELETE the Todo item.

At the end, we are using `redirect` to navigate the user back from the server action call. This is needed as we don’t want the user to use a browser page refresh to create or delete the Todo item again, reusing the form submit.

We also implemented the `allTodos` function here, to have all the storage related code in a single file.

## Add new item

To implement the AddTodo component, create an **src/AddTodo.tsx** file with the following content:

```jsx
import { useActionState } from "@lazarv/react-server/router";
import type { ZodIssue } from "zod";

import { addTodo } from "./actions";

export default function AddTodo() {
  const { formData, error } = useActionState<
    typeof addTodo,
    string & Error & ZodIssue[]
  >(addTodo);

  return (
    <form action={addTodo} className="mb-4">
      <div className="mb-2">
        <input
          name="title"
          type="text"
          className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg p-2.5"
          defaultValue={formData?.get?.("title") as string}
          autoFocus
        />
      </div>
      <button
        className="text-white bg-blue-700 hover:bg-blue-800 rounded-lg px-5 py-2 mb-2 text-center"
        type="submit"
      >
        Submit
      </button>
      {error?.map?.(({ message }, i) => (
        <p
          key={i}
          className="bg-red-50 border rounded-lg border-red-500 text-red-500 p-2.5 mb-2"
        >
          {message}
        </p>
      )) ??
        (error && (
          <p className="bg-red-50 border rounded-lg border-red-500 text-red-500 p-2.5">
            {error}
          </p>
        ))}
    </form>
  );
}
```

We already know, how to use server actions from a `<form>`. But what about the result of our server action call? `useActionState` to the rescue!

By passing the `addTodo` server action function reference to `useActionState` , we can get the result of the server action call when this specific server action was called, so we can collect the `error` result. This will be the Zod error issues we thrown in the add server action. So we can iterate on all Zod validation issues here and render validation error messages on the server side.

## Production build

During using the development server, you can notice that the page loaded some JavaScript modules in the browser. This is only used for Hot Module Replacement. In a production build, only the document and a CSS asset will be loaded in the browser.

To build for production, run `pnpm build` and then you can start the production server using `pnpm start` . 

## Final words

That’s it! We have a working small Todo example. You can also find the example on [GitHub](https://github.com/lazarv/react-server/tree/main/examples/todo). By using the above approach, it will be easy for you to add a Todo item update feature, marking an item as completed. I hope you had fun and you will like the developer experience the [@lazarv/react-server](https://react-server.dev) framework provides! There are a lot of other exciting features provided by React and the framework itself! We don’t used any client components here, that will be in another tutorial. The documentation site of `@lazarv/react-server` was created using the framework and it’s build and deployment to Vercel takes about 5s! Because the framework uses Vite, the developer experience is blazing-fast!