Receive input and add to database

We already have a loader() that can access the database to fetch data. But we need a way to send data to the database.

Let’s add a function to app/utilities/db.ts to address this. Before we do that, we’ll create an interface to define the contents of a task, to make it easier and less error prone.

Create app/utilities/interfaces.ts with this content:

export interface ITask {
    id: number;
    task: string;
    deadline: string;
  }

I named it ITask, with a capital I to show that it is an interface and not a type alias.

With the interface in place, we turn our attention to app/utilities/db.ts.

Import our new interface at the top, and then add a function to create tasks in the database:

import { ITask } from "./interfaces";

export async function createTask(task: ITask): Promise<void> {
    await db.tasks.create({data: {task: task.task, deadline: task.deadline }});
    db.$disconnect();
}

The return type will be a Promise, since it is an async function, and the contents of that Promise is void. Once we’ve added the data, we disconnect from the db. This way we have the smallest number of simultaneous connections to the database.

Now we need to find a way to invoke this function, and feed it the correct data. This has to happen in our app/routes/index.ts.

The easiest way is to first change the form to the Remix Form component. You do that by first importing it from Remix, and then capitalise the tag name in your code:

import { Form, useLoaderData } from "remix";

export default function Index() {
  let todoList = useLoaderData();
  return (
    <div>
      <div>
        <div>
          <Form method="post">
          <input
            type="text"
            placeholder="Task..."
            name="task"
          />
          <input
            type="text"
            placeholder="Deadline (in Days)..."
            name="deadline"
          />
        <button type="submit">Add Task
          </button>
        </Form>
        </div>
      </div>
      <div>
        {todoList.map((task: any) => {
          return (
            <div key={task.id} className="task">
      <div>
        <span>{task.task}</span>
        <span>{task.deadline}</span>
        <button>X</button>
      </div>

    </div>
          );
        })
      }
      </div>
    </div>
  );
}

Next you need to receive and process the formdata. You do that in a function called action().

export async function action({ request }:any) {
  let formData = await request.formData();
  const {_action, ...values} = Object.fromEntries(formData);
  if(_action === "create"){
    createTask(values as ITask);
  }
return {};
}

As you can see, there is a variable _action. It needs to have the value create in order for the function to call our createTask(). Where does that come from? If you look at MDN button documentation you’ll see that buttons can have values connected to their name. If we give the “Add Task…” button the name _action and the value create, that’ll do the trick for us.

... (stuff omitted)
<button type="submit" name="_action" value="create" >Add Task
</button>

You can use any name you want, as long as you use the same name on the variable in your action(). I chose _action because it describes how I intend to use the value.

If you fire up npm run dev now you should be able to add tasks to your list. But you can’t delete them yet.

Let’s fix that now!

We need a function to delete tasks, let’s add it to app/utilities/db.ts:

export async function deleteTask(ID: number): Promise<void> {
    await db.tasks.delete({
        where: { id: ID as number }
    });
    db.$disconnect();
}

We need the ID the task has in the database, so that has to be a parameter.

How do we get hold of that, and how can we set up the delete button to actually work? It’s not even inside a form, so what can we do?

Well, first off we need to surround it with a form, or rather a Form. Then we need a hidden input to carry the ID value.

Finally, we need to give the button itself a value of delete to separate the actions in our action().

Let’s also extract the entire task to a separate component, components/TodoTask.tsx.

import { Form } from "remix";
import { ITask } from "../utilities/interfaces";
import React from "react";

interface Props {
  task: ITask;
}

const TodoTask = ({ task }: Props) => {
  return (
    <div key={task.id}>
      <div>
      <Form method="post">
          <input type="hidden" name="ID" value={task.id}></input>
        <span>{task.task}</span>&nbsp;
        <span>{task.deadline}</span>&nbsp;
      <button
        type="submit" name="_action" value="delete"
      >×</button></Form>
      </div>
    </div>
  );
};

export default TodoTask;

As you see, we wrap the button in a Form and add an input of type “hidden”. That way we can send the tasks ID to our action(). We give the button the same name as our previous button, ”_action”. This lets us filter out the action to take, based on the value of ”_action”.

We’ll do that in a minute, but let us first use the new TodoTask.

Let’s replace the inline version we have in routes/index.tsx.

Remove this code:

<div>
        {todoList.map((task: any) => {
          return (
            <div key={task.id}>
      <div>
        <span>{task.task}</span>
        <span>{task.deadline}</span>
        <button>×</button>
      </div>
    </div>
          );
        })
      }
</div>

And replace it with this:

<div>
        {todoList.map((task: ITask, key: number) => {
          return <TodoTask key={key} task={task} />;
        })}
</div>

Remember to also import the new TodoTask component at the top of routes/index.tsx:

import { ITask } from "~/utilities/interfaces";

Everything should look the same in the browser when you save. If the site isn’t running, run npm run dev in the terminal to check.

All that remains to make deletions work now is to add the required code inside our action().

export async function action({ request }:any) {
  let formData = await request.formData();
  const {_action, ...values} = Object.fromEntries(formData);
  if(_action === "create"){
    createTask(values as ITask);
  }
  if(_action=== "delete") {
    try {
      let ID = parseInt(values.ID);
      await deleteTask(ID);
      } catch (e) {
        return {error: true};
      }
  }
return {};
}

Remember to also import deleteTask from ~/utilities/db:

import { createTask, deleteTask, getTasks } from "~/utilities/db";

Notice that we need to use parseInt() on values.id, because we get it as string, but we need a number to make it work.

Go ahead and test it in the browser now, it should be working. But it isn’t all that it could be. If you throttle the browser to “slow 3G” you’ll notice it takes a while for things disappear when you delete them. As it also does when you add new tasks. But there is no feedback at all. We’ll deal with that in the next part of this series.

About the author

For the last three decades, I've worked with a variety of technologies. I'm currently focused on fullstack development. On my day to day job, I'm a senior teacher and course developer at a higher vocational school in Malmoe, Sweden. I'm also an occasional tech speaker and a mentor. Do you want to know more? Visit my website!