How to Use React Router for Routing and as a Framework with Appwrite Part 2

Immanuel GoldsonImmanuel Goldson
21 min read

In the first part of this tutorial, we covered a lot about React Router and explored its declarative(library) mode. We started by setting up React Router's library mode, then built routes and components along with links and navigation. Then did something similar with React Router's framework mode, exploring the differences, features, components, and routes.

For this part of the tutorial, we dive deeper into the framework mode, exploring data fetching and mutations in React Router v7 with loaders, actions, and finally build a Todo list project using React Router's framework mode and Appwrite to practice what we learned. Click on this link to visit the live project.

The GitHub repository for this part of the article can be found at this link. This is the GitHub repository link for the project we will create.

Data Fetching and Mutations

React Router takes a different approach to data fetching and mutation than plain React. It makes use of loaders and actions to render or modify data.

Using Loaders

Data is retrieved in React Router by the loader and client loader. Loader data is serialized from loaders out of the box and deserialized into components.

Loaders can return basic values like strings and numbers, as well as promises, maps, sets, and more.

Client Data Loading

This is used to fetch data on the client. It is really useful for projects or pages that you would prefer to fetch data on the browser only, such as fetching data from a third-party API.

For us to test how client-side data loading works, go back to the contact route module we created earlier and paste it in the clientLoader:

import { useState } from "react";
import type { Route } from "./+types/contact";

export const clientLoader = async () => {
  try {
    const res = await (await fetch("https://jsonplaceholder.typicode.com/users")).json()
      .then((data) => {
        console.log(data);
        return data;
      });

    return res;
  } catch (error) {
    console.log(error);
  }
};
const contact = ({ loaderData }: Route.ComponentProps) => {
  console.log(loaderData);
  return (
    <main className="">
      <h2 className="text-[2.4rem] font-bold p-[22px]"> Contact </h2>
    </main>
  );
};

export default contact;

The clientLoader function fetches and returns data from the JSONPlaceholder user API endpoint. The contact route module then displays this API data using the loaderData parameter provided in the contact route module. Once you navigate back to the contact route, our page should display a list of fake users:

Server loaders, on the other hand, are used to fetch data from the server. This is useful for projects or pages that make use of database integration, such as using a database to retrieve documents in a project. We will explore server loaders later in the project.

Using Action

Data is mutated through Route actions. When the actions complete, all loader data on the page is revalidated to keep your UI in sync. Route actions are defined as action and are called on the server, while the actions called on the browser are defined as clientAction.

Client actions only run in the browser and are prioritized over server actions. To test client actions, in the same contact component where I added loaders, paste the following code to add a clientAction:

export const clientAction = async () => {
  try {
    await fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      body: JSON.stringify({
        id: 1,
        title: "New Post!",
        body: "New Body...",
        userId: 1,
      }),
      headers: {
        "Content-type": "application/json",
      },
    }).then((res) => {
      if (res.status === 201) {
        console.log("Post Created!");
      }
    });
  } catch (error) {
    console.log(error);
  }
};

In this code snippet, we make use of the JSON placeholder post API endpoint and make a fake POST response:

await fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      body: JSON.stringify({
        id: 1,
        title: "New Post!",
        body: "New Body...",
        userId: 1,
      }),
      headers: {
        "Content-type": "application/json",
      },
    }).then((res) => {
      if (res.status === 201) {
        console.log("Post Created!");
      }
    });

Once the response is successful, it will return a status of 201. We check for this with an if statement. When the status is 201, the console logs the message "Post Created!".

Next, add a Form component and a Create Post button to the UI:

<Form method="post">
          <button
            className="py-[8px] px-[12px] bg-black text-white text-[1.4rem] font-bold rounded-md cursor-pointer"
            type="submit"
          >
            Create Post
          </button>
        </Form>

Now, when you go to the contact route and click the Create Post button to call the clientAction, a post request is sent, and the console logs the message "Post Created!".

We will also explore server actions later since we are using a database in the project.

Click on this link to view and test what we have learned in the framework mode.

Building a To-do List Project with React Router

In order to practice what we have learned so far, we will build a basic to-do list app with React Router.

For this section of the tutorial, we use React Router along with Appwrite as our database (though you don't need to know how to use Appwrite, as we will only use its CRUD features to demonstrate how React Router can be used in a real-world project).

To view the complete To-do list Project, click on this link

Setting Up The Project

To set up the To-do list project, create a new React Router project with your chosen folder name using Vite, or manually with React Router’s standard installation command, as we did earlier.

Next, install the Appwrite package into our project with the following command:

pnpm add appwrite

Now, navigate to Appwrite’s website and create a new account. Once the account is created, you will either be automatically sent to the Appwrite console, or you can go to Appwrite’s console with the Go to Console button once you sign up:

In the process of setting up the Appwrite project, do the following:

Click on the Create project button to create a new Appwrite project:

Once the Create project button is clicked and you have written a name for the project and selected the server region, you can now click on the Create button:

Next, select the platform you will want to use Appwrite on; in this case, choose “web“ since we are working with React Router:

Then, choose React as the library to use with Appwrite:

Once you click on the “create platform“ button, Appwrite will take you to another page that contains the necessary credentials and steps to connect the React App. The page also shows the setup status, which must be successfully connected before you can use Appwrite with React Router:

Now, create a new file called appwrite.ts and import the appwrite package in the folder of our project to configure Appwrite. Then, paste in the following code:

import { Client} from "appwrite";

const client = new Client();

client
  .setEndpoint("Endpoint") 
  .setProject("Project_ID");

export { client };

In the code snippet, we create and export a variable called client and assign it to a new instance of the Client class we imported from the appwrite package.

We call the client variable with the Appwrite project’s endpoint and project ID, like we saw earlier.

You will also need to send a ping to Appwrite in the project before Appwrite can be connected with the project. To do so, create a sendPing function in the home.tsx component and paste the following code into it:

const sendPing = async () => {
  client.ping();
};

Note: Remove all the unnecessary starter code that comes with creating a new React Router App. Your project folder should look like this:

Then, create a button with the following JSX elements and add the sendPing function we created earlier in the home.tsx route module as a click event to the button:

import { client } from "~/appwrite";
import type { Route } from "./+types/home";

const sendPing = async () => {
  client.ping();
};

export default function Home() {
  return (
    <main className="h-[100vh] w-full flex flex-col gap-y-[12px] justify-center items-center p-[8px]">
      <h2>Home</h2>
      <button
        className="bg-[#f1f1f1] py-[8px] px-[12px] rounded-md cursor-pointer"
        onClick={sendPing}
      >
        Send Ping
      </button>
    </main>
  );
}

Now, go to localhost, and when you click the sendPing button, return to the Appwrite setup page. It should be connected. If the changes don't appear after a few seconds or a minute, try reloading the page.

Once the connection is successful, remove the sendPing button and function.

Using Appwrite’s Database Features

We will also need to use Appwrite’s database features, so go back to the setup page and click on the database section. Then, create a new database with the name “Todo” and a collection with the name “Todo items”:

Next, create a new variable with an imported new instance of the Databases class from Appwrite in our appwrite.ts file and paste the client variable we created earlier into the parentheses of the Databases class:

import { Client, Databases } from "appwrite";

const client = new Client();

client
  .setEndpoint("Endpoint") // Your Appwrite Endpoint
  .setProject("Project_ID");

const databases = new Databases(client);

export { client };

export default databases;

We are using Appwrite to test how React Router works, so we won't be using authentication. To use Appwrite's database without authentication, go to the database settings and enable permissions for creating, reading, updating, and deleting items in the database for any user:

Adding an Attribute

Now that we have created a new database, click on the create attribute button to create an attribute that the database will use in creating new to-do list items:

Then, select the new database attribute as a string since the todo items will be text:

Once you have selected the data type as a string, a new modal appears with options to enter an attribute key, the size of the string, the default content for the attribute, and whether it should be required or an array.

Now, write “todo“ as the attribute key, then 100-10000 as the size of the string, finally set the attribute to required and click on the Create button:

The attribute has now been successfully created. You can add it to Appwrite’s method whenever you need to use a database feature on the todo attribute:

Creating Simple Routes and Components

Change the home.tsx component to alltodos.tsx and change the index route module that initially had its route file path as routes/home.tsx to routes/alltodos.tsx. In the alltodos.tsx file, paste the following code:

import type { Route } from "../+types/root";

const alltodos = () => {

  return (
    <main className=" min-h-[100vh] w-full flex p-[26px] bg-black text-white">
        <h2 className=" text-[2.2rem] font-bold">All Todos</h2>
    </main>
  );
};

export default alltodos;

Once you have the alltodos index route module, navigate to it on your browser, your page should display this:

Creating a new To-do List Item

We set up a new route module and route to create a new to-do list item. As i mentioned earlier, React Router uses actions and loaders to handle data.

Now, create a new route module called newtodoitem and set up the route module with the file path pointing to this component. The route's path should also be set to "new":

  route("new", "routes/newtodoitem.tsx"),

Next, paste the following code into the newtodoitem route module:

import { Form, Link, redirect, type ActionFunctionArgs } from "react-router";

import databases from "~/appwrite";

import { ID } from "appwrite";

export const action = async ({ request }: ActionFunctionArgs) => {
  try {
    const formData = await request.formData();

    const todo = formData.get("todo-value") as string;

    if (!todo) {
      return { error: "Fill in a Todo!" };
    }

    await databases.createDocument(
      "Database_ID",
      "Collection_ID",
      ID.unique(),
      { todo }
    );

    return redirect("/");
  } catch (error) {
    return { error };
  }
};

const newtodo = () => {
  return (
    <main className="flex flex-col gap-y-[36px] p-[22px]">
      <section className="flex justify-between items-center">
        <h2 className="text-[2.6rem] font-bold">Create New Todo</h2>

        <Link
          to="/"
          className="text-[1.4rem] font-bold bg-[#111] py-[8px] px-[14px] rounded-md"
        >
          Go Back
        </Link>
      </section>
      <Form method="post" className="flex flex-col gap-y-[12px]">
        <input
          type="text"
          name="todo-value"
          placeholder="New Todo..."
          className="border-white border-[1.3px] py-[8px] px-[16px] rounded-md"
        />
        <button
          type="submit"
          className="self-start bg-[#111] py-[8px] px-[16px] rounded-md cursor-pointer"
        >
          Add Todo
        </button>
      </Form>
    </main>
  );
};

export default newtodo;

Let me break down what is happening in this code snippet:

We first create a server action because we are working with Appwrite. In the action function, we create a new variable called formData, which uses the request parameter in our function.

export const action = async ({ request }: ActionFunctionArgs) => {
  try {
    const formData = await request.formData();
} catch (error) {
    return { error };
  }
};

Check to see if the todo variable currently has a value, which means we are checking to see if any content has been written in the input box:

if (!todo) {
      return { error: "Fill in a Todo!" };
    }

Once we have added the if statement for that, we can move on to adding the database functionality, since we want to be able to create new to-do list items, import the database variable we exported from the appwrite.ts config file, along with an id variable from Appwrite.

Then we use Appwrite’s createDocument() method, which takes the database ID, collection ID, the ID imported from Appwrite, and the todo variable from the form. The todo variable is placed where the attribute is, and we only write it once since their names are identical.

    await databases.createDocument(
      "Database_ID",
      "Collection_ID",
      ID.unique(),
      { todo }
    );

Then, add some JSX elements, including the links, input box and button we will use:

Finally, if everything works smoothly. When the new to-do list item is created, the page will redirect to the alltodos route module with the new to-do list item:

Retrieving all the To-do List items

We just added the ability to create new to-do list items to our project, but we would also need to be able to view them in our “/alltodos“ route module.

Create a new loader function in our alltodos route module with the following code:

import { ID } from "appwrite";
import databases, { client } from "~/appwrite";
import type { Route } from "../+types/root";
import { Link, NavLink } from "react-router";

export const loader = async () => {
  return await databases.listDocuments(
    "Database_ID",
    "Collection_ID",
  );
};

const alltodos = ({ loaderData }: Route.ComponentProps) => {
  return (
    <main className=" min-h-[100vh] w-full flex flex-col p-[26px] bg-black">
      <section className="flex justify-between items-center mb-[22px]">
        <h2 className=" text-[2.2rem] font-bold">All Todos</h2>
        <Link
          to="/new"
          className="text-[1.6rem] font-bold cursor-pointer bg-[#111] text-white py-[8px] px-[16px] rounded-md"
        >
          New Todo
        </Link>
      </section>
      {loaderData.documents?.map((todo: { $id: string; todo: string }) => (
        <Link to={`/todos/${todo.$id}`} className="-">
          <h3 className="text-[1.6rem] font-[600] text-[#f1f1f1]">{todo.todo}</h3>
        </Link>
      ))}
    </main>
  );
};

export default alltodos;

We created the loader function, like we learned earlier, which is used to fetch data, and in this case, we are retrieving the to-do list items from Appwrite:

export const loader = async () => {
  return await databases.listDocuments(
    "Database_ID",
    "Collection_ID",
  );
};

The loader function returns the appwrite listDocument() method, this method is used to return all the documents in a collection. It takes in the database ID and collection ID.

The alltodos component then uses its loaderData parameter to get the data returned from the to-do, which is an array of all the to-do list items:

{loaderData.documents?.map((todo: { $id: string; todo: string }) => (
        <Link to={`/todos/${todo.$id}`} className="-">
          <h3 className="text-[1.6rem] font-[600] text-[#f1f1f1]">{todo.todo}</h3>
        </Link>
      ))}

Paste in the following JSX elements to render the to-do list items from the loaderData parameter:

 <main className=" min-h-[100vh] w-full flex flex-col p-[26px] bg-black">
      <section className="flex justify-between items-center mb-[22px]">
        <h2 className=" text-[2.2rem] font-bold">All Todos</h2>
        <Link
          to="/new"
          className="text-[1.6rem] font-bold cursor-pointer bg-[#111] text-white py-[8px] px-[16px] rounded-md"
        >
          New Todo
        </Link>
      </section>
      {loaderData.documents?.map((todo: { $id: string; todo: string }) => (
        <Link to={`/todos/${todo.$id}`} className="-">
          <h3 className="text-[1.6rem] font-[600] text-[#f1f1f1]">{todo.todo}</h3>
        </Link>
      ))}
    </main>

Now, once you create a new to-do list item, you get redirected back to the alltodos route module, which displays all the to-do list items you have created:

Retrieving a To-do List Item

We retrieved all the to-do list items in the alltodos route module in the last section, however, we are going to improve our to-do list app by allowing users to view an individual todo item:

Create a todo route and a todo route module, then paste in the following code:

  route("todos/:id", "routes/todo.tsx"),
import databases from "~/appwrite";
import type { Route } from "./+types/todo";
import { Form, Link, redirect, type ActionFunctionArgs } from "react-router";

export const loader = async ({ params }: Route.LoaderArgs) => {
  const id = params.id;

  const todo = await databases.getDocument(
     "Database_ID",
    "Collection_ID",
    id
  );

  return { todo };
};

export const action = async ({ request, params }: ActionFunctionArgs) => {
  const formData = await request.formData();

  const todo = formData.get("todo-value");

  if (!todo) {
    return { error: "Fill In a Todo!" };
  }

    return redirect("/");
  }
};

const todo = ({ loaderData, actionData }: Route.ComponentProps) => {
  console.log(loaderData);

  return (
    <main className="h-[100vh] w-full bg-black flex flex-col gap-y-[22px] p-[22px]">
      <section className="flex justify-between items-center">
        <h2 className="text-[2.2rem] font-bold text-[#f1f1f1]">{loaderData.todo.todo}</h2>
        <Link
          to="/"
          className="text-[1.4rem] font-bold cursor-pointer bg-[#111] text-white py-[8px] px-[16px] rounded-md"
        >Back Home</Link>
      </section>
    </main>
  );
};

export default todo;

Just like in the previous section, we create a loader function in the todo.tsx file. However, this time we use Appwrite's getDocument() method to retrieve data documents by their ID. Paste in the following code to get the ID of the route module:

export const loader = async ({ params }: Route.LoaderArgs) => {
  const id = params.id;
};

If you remember earlier in this tutorial, we learnt how to retrieve the ID of a route when we were learning about dynamic routes.

Now, once you add the ID to the getDocument() method along with the document ID and collection ID, Appwrite will know the to-do item to retrieve:

 const todo = await databases.getDocument(
   "Database_ID",
    "Collection_ID",
    id
  );

Paste in the following JSX elements with the to-do item we retrieved for us to view:

 <main className="h-[100vh] w-full bg-black flex flex-col gap-y-[22px] p-[22px]">
      <section className="flex justify-between items-center">
        <h2 className="text-[2.2rem] font-bold text-[#f1f1f1]">{loaderData.todo.todo}</h2>
        <Link
          to="/"
          className="text-[1.4rem] font-bold cursor-pointer bg-[#111] text-white py-[8px] px-[16px] rounded-md"
        >Back Home</Link>
      </section>
    </main>

Next, update all the to-do's JSX elements with a link. This way, when you click on it, you'll be taken to the todo route for the specific to-do you selected.

Updating a To-do List Item

In the previous section, we learned how to retrieve a to-do list item with React Router and Appwrite, but in this section, we will update that same to-do list item.

In the todo.tsx route module we created earlier to retrieve a to-do list item, create an action function and render a Form component, then paste in the following JSX elements into the Form component:

import databases from "~/appwrite";
import type { Route } from "./+types/todo";
import { Form, Link, redirect, type ActionFunctionArgs } from "react-router";

export const loader = async ({ params }: Route.LoaderArgs) => {
  const id = params.id;

  const todo = await databases.getDocument(
      "Database_ID",
    "Collection_ID",
    id
  );

  return { todo };
};

export const action = async ({ request, params }: ActionFunctionArgs) => {
  const formData = await request.formData();

  const todo = formData.get("todo-value");

  if (!todo) {
    return { error: "Fill In a Todo!" };
  }

    const response = await databases.updateDocument(
       "Database_ID",
    "Collection_ID",
      params.id,
      { todo }
    );

    return { updated: true };

};

const todo = ({ loaderData, actionData }: Route.ComponentProps) => {
  console.log(loaderData);

  return (
    <main className="h-[100vh] w-full bg-black flex flex-col gap-y-[22px] p-[22px]">
      <section className="flex justify-between items-center">
        <h2 className="text-[2.2rem] font-bold text-[#f1f1f1]">{loaderData.todo.todo}</h2>
        <Link
          to="/"
          className="text-[1.4rem] font-bold cursor-pointer bg-[#111] text-white py-[8px] px-[16px] rounded-md"
        >Back Home</Link>
      </section>

      {actionData?.updated && (
        <p className="text-[1.6rem] font-bold text-green-500">
          Todo Updated Successfully!
        </p>
      )}

      <Form method="post" className="flex flex-col gap-y-[22px]">
        <h2 className="text-[1.6rem] font-[600]">Edit Todo</h2>
        <input
          type="text"
          name="todo-value"
          defaultValue={loaderData.todo.todo}
          className="border-white border-[1.3px] py-[8px] px-[16px] rounded-md"
        />

        <section className="flex items-center self-start gap-x-[32px] [&>button]:bg-[#111] [&>button]:py-[8px] [&>button]:px-[16px] [&>button]:rounded-md [&>button]:cursor-pointer">
          <button type="submit" value="update">
            Update Todo
          </button>
        </section>
      </Form>
    </main>
  );
};

export default todo;

In this code snippet, we retrieve the ID and get the specific to-do list Item as we did in the previous section:

export const loader = async ({ params }: Route.LoaderArgs) => {
  const id = params.id;

  const todo = await databases.getDocument(
    "Database_ID",
    "Collection_ID",
    id
  );

  return { todo };
};

Then, create an action to get the value from the input box with a todo variable. Then, insert an input element into the UI and use it to update the to-do list item with the new value using the appwrite’s updateDocument() method. This method requires a database ID, a collection ID, and the params.id, we get from the action function's parameter:

export const action = async ({ request, params }: ActionFunctionArgs) => {
  const formData = await request.formData();

  const todo = formData.get("todo-value");

  if (!todo) {
    return { error: "Fill In a Todo!" };
  }

    const response = await databases.updateDocument(
        "Database_ID",
    "Collection_ID",
      params.id,
      { todo }
    );

    return { updated: true };

};

I added an input element and a button to the UI for a user to be able to fill in a new to-do, and update the existing to-do once the button is clicked:

 <main className="h-[100vh] w-full bg-black flex flex-col gap-y-[22px] p-[22px]">
      <section className="flex justify-between items-center">
        <h2 className="text-[2.2rem] font-bold text-[#f1f1f1]">{loaderData.todo.todo}</h2>
        <Link
          to="/"
          className="text-[1.4rem] font-bold cursor-pointer bg-[#111] text-white py-[8px] px-[16px] rounded-md"
        >Back Home</Link>
      </section>

      {actionData?.updated && (
        <p className="text-[1.6rem] font-bold text-green-500">
          Todo Updated Successfully!
        </p>
      )}

      <Form method="post" className="flex flex-col gap-y-[22px]">
        <h2 className="text-[1.6rem] font-[600]">Edit Todo</h2>
        <input
          type="text"
          name="todo-value"
          defaultValue={loaderData.todo.todo}
          className="border-white border-[1.3px] py-[8px] px-[16px] rounded-md"
        />

        <section className="flex items-center self-start gap-x-[32px] [&>button]:bg-[#111] [&>button]:py-[8px] [&>button]:px-[16px] [&>button]:rounded-md [&>button]:cursor-pointer">
          <button type="submit" value="update">
            Update Todo
          </button>
        </section>
      </Form>
    </main>

The page will also render a text of “update successful” once the button is clicked:

Deleting a To-do list Item

Deleting to-do list Items from our database involves getting the ID of the to-do list item with a loader and using Appwrite to remove the item based on that same ID.

To delete a to-do list item in our project, paste the following code into the todo.tsx route module we used to retrieve and update the to-do list items earlier:

import databases from "~/appwrite";
import type { Route } from "./+types/todo";
import { Form, Link, redirect, type ActionFunctionArgs } from "react-router";

export const loader = async ({ params }: Route.LoaderArgs) => {
  const id = params.id;

  const todo = await databases.getDocument(
      "Database_ID",
    "Collection_ID",
    id
  );

  return { todo };
};

export const action = async ({ request, params }: ActionFunctionArgs) => {
  const formData = await request.formData();

  const todo = formData.get("todo-value");

  const intent = formData.get("intent");

  if (!todo) {
    return { error: "Fill In a Todo!" };
  }

  if (intent === "update") {
    const response = await databases.updateDocument(
       "Database_ID",
    "Collection_ID",
      params.id,
      { todo }
    );

    return { updated: true };
  } else if (intent === "delete") {
    const response = await databases.deleteDocument(
   "Database_ID",
    "Collection_ID",
      params.id
    );

    return redirect("/");
  }
};

const todo = ({ loaderData, actionData }: Route.ComponentProps) => {
  console.log(loaderData);

  return (
    <main className="h-[100vh] w-full bg-black flex flex-col gap-y-[22px] p-[22px]">
      <section className="flex justify-between items-center">
        <h2 className="text-[2.2rem] font-bold text-[#f1f1f1]">{loaderData.todo.todo}</h2>
        <Link
          to="/"
          className="text-[1.4rem] font-bold cursor-pointer bg-[#111] text-white py-[8px] px-[16px] rounded-md"
        >Back Home</Link>
      </section>

      {actionData?.updated && (
        <p className="text-[1.6rem] font-bold text-green-500">
          Todo Updated Successfully!
        </p>
      )}

      <Form method="post" className="flex flex-col gap-y-[22px]">
        <h2 className="text-[1.6rem] font-[600]">Edit Todo</h2>
        <input
          type="text"
          name="todo-value"
          defaultValue={loaderData.todo.todo}
          className="border-white border-[1.3px] py-[8px] px-[16px] rounded-md"
        />

        <section className="flex items-center self-start gap-x-[32px] [&>button]:bg-[#111] [&>button]:py-[8px] [&>button]:px-[16px] [&>button]:rounded-md [&>button]:cursor-pointer">
          <button type="submit" name="intent" value="update">
            Update Todo
          </button>
        </section>
      </Form>
    </main>
  );
};

export default todo;

We added a delete button to our UI. To toggle between the update and delete buttons, I assigned the name "intent" to the button. Then, I used a conditional statement to check which button was clicked and perform different actions based on the selected button:

export const action = async ({ request, params }: ActionFunctionArgs) => {
  const formData = await request.formData();

  const todo = formData.get("todo-value");

  const intent = formData.get("intent");

  if (!todo) {
    return { error: "Fill In a Todo!" };
  }

  if (intent === "update") {
    const response = await databases.updateDocument(
       "Database_ID",
    "Collection_ID",
      params.id,
      { todo }
    );

    return { updated: true };
  } else if (intent === "delete") {
  }
};

Appwrite also provides the deleteDocument() method, which can remove documents based on their ID, so paste this into the else intent === “delete“ statement for when we click on the delete button:

 const response = await databases.deleteDocument(
        "Database_ID",
    "Collection_ID",
      params.id

Then add a delete button to the UI, with the code below:

  <section className="flex items-center self-start gap-x-[32px] [&>button]:bg-[#111] [&>button]:py-[8px] [&>button]:px-[16px] [&>button]:rounded-md [&>button]:cursor-pointer">
          <button type="submit" name="intent" value="update">
            Update Todo
          </button>
          <button type="submit" name="intent" value="delete">
            Delete Todo
          </button>
        </section>

Finally, React Router redirects the user back to the alltodos route once the button is clicked, displaying all the remaining todo items:

Click on this link to view and test out the To-do-list project.

Benefits and Drawbacks

We have learnt so much about React Router, so here are a few advantages of using it:

  • Familiar Syntax: Since React Router V7 is a combination of Remix and React Router, it will be easier to understand React Router V7 if you are familiar with any of them.

  • Community: React Router has always had a large community because it is the most used routing library in React. Now, it also includes developers from Remix, making it even bigger.

  • Developer Experience: Easy-to-use routing system, and built-in features like error boundaries, Form fetchers and form validation helpers contribute to a more intuitive and productive development workflow.

  • SEO Optimization: Features like Server-side rendering and focus on web standards in React Router V7 contribute to better SEO performance

  • Flexibility: React Router V7 can be used in library mode, framework mode and even data mode, allowing developers to use either one of the modes depending on the use case or project requirements.

Using React Router as a framework for the most part has lots of benefits, but here are some scenarios where it falls short:

  • Still New: Since React Router V7 is quite new, it might take some time for people to start using it widely, even though it combines two popular tools.

  • Steep learning curve: Although React Router V7 is easier to understand if you are familiar with React Router and Remix, it can still be confusing due to its multiple modes and new features.

  • Harder to migrate for Remix or React Router V6 apps: Apps built deeply with either React Router V6 and Remix might find it hard or time-consuming to start porting their code to work with React Router V7

  • Confusing Docs: React Router V7 introduces new documentation and three modes for using React Router: library mode, framework mode, and data mode. This can be confusing for developers.

Conclusion

React Router has long been a big name in the React space as a routing library. However, since Remix and React Router were both created by the same team, and Remix utilises many React Router features under the hood, the team decided to merge them as of November 2024.

React router as a framework is still relatively new but it comes with great developer experience and similar components and hooks if you are coming from react router as a routing library and Remix, in this tutorial we learnt about react router and using both the Declarative(library) mode for creating routes across your website and as a react framework for building web apps with the framework mode. We also later built a To-do list project with the framework mode and Appwrite to summarise what we learned.

If you made it this far to the tutorial and found it helpful, feel free to like and comment. Thank you!

Next Steps and Resources

If you want to learn more about React Router and its different modes, including its data mode that we couldn’t check out due to the scope of this tutorial, do well to check out the following resources:

0
Subscribe to my newsletter

Read articles from Immanuel Goldson directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Immanuel Goldson
Immanuel Goldson