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


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:
Subscribe to my newsletter
Read articles from Immanuel Goldson directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
