React Router v6.4 introduced the Data APIs — a major upgrade that lets us handle data fetching, mutations (actions), and error handling directly in our route definitions.
In this tutorial, we’ll build a small demo app using Vite + React Router + JSON Server.
What is createBrowserRouter
?
createBrowserRouter
is a function introduced in React Router v6.4 as part of the new Data APIs.
It:
- Defines all routes in a single configuration object
- Supports data loading, error handling, and actions
- Replaces the older
<BrowserRouter> + <Routes> + <Route>
setup - Must be combined with
<RouterProvider>
This means routing now goes beyond navigation — it’s also about data + UI orchestration.
Installation & Setup
# Create Vite project
npm create vite@latest react-data-api-demo
cd react-data-api-demo
npm install
# Install dependencies
npm install react-router-dom
# Install JSON Server for fake backend
npm install -g json-server
JSON Server Setup
Create a file named db.json
in the project root:
{
"users": [
{
"id": "1",
"name": "Leanne Graham",
"email": "Sincere@april.biz"
},
{
"id": "2",
"name": "Ervin Howell",
"email": "Shanna@melissa.tv"
}
]
}
Run JSON Server:
json-server --watch db.json --port 5000
Project Structure
src/
├── App.jsx
├── main.jsx
├── router.js
├── pages/
│ ├── Users.jsx
│ ├── NewUserForm.jsx
│ └── ErrorPage.jsx
└── db.json
Code
main.jsx
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import router from "./router";
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
router.js
import { createBrowserRouter } from "react-router-dom";
import App from "./App";
import ErrorPage from "./pages/ErrorPage";
import Users, { usersLoader } from "./pages/Users";
import NewUserForm, { addUserAction } from "./pages/NewUserForm";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
children: [
{
path: "users",
element: <Users />,
loader: usersLoader, // GET -> fetch users
},
{
path: "users/new",
element: <NewUserForm />,
action: addUserAction, // POST -> add user
},
],
},
]);
export default router;
App.jsx
import { NavLink, Outlet } from "react-router-dom";
const App = () => {
return (
<div style={{ padding: "20px" }}>
<h1>🚀 React Router Loader + Action Example</h1>
{/* Navigation */}
<nav style={{ marginBottom: "20px" }}>
<NavLink to="/" style={{ marginRight: "10px" }}>
Home
</NavLink>
<NavLink to="/users" style={{ marginRight: "10px" }}>
Users
</NavLink>
<NavLink to="/users/new">Add User</NavLink>
</nav>
{/* This is where child routes will render */}
<Outlet />
</div>
);
};
export default App;
pages/Users.jsx
import { useLoaderData, Link } from "react-router-dom";
export const usersLoader = async () => {
const response = await fetch("http://localhost:5000/users");
if (!response.ok) {
throw new Response("Failed to load users", { status: response.status });
}
return response.json();
};
export default function Users() {
const users = useLoaderData();
return (
<div>
<h2>Users</h2>
<Link to="/users/new">➕ Add New User</Link>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
pages/NewUserForm.jsx
import { Form, redirect } from "react-router-dom";
export const addUserAction = async ({ request }) => {
const formData = await request.formData();
const newUser = {
name: formData.get("name"),
email: formData.get("email"),
};
// Fake POST request
const response = await fetch("http://localhost:5000/users", {
method: "POST",
body: JSON.stringify(newUser),
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
throw new Response("Failed to add user", { status: response.status });
}
// After success -> redirect back to users list
return redirect("/users");
};
const NewUserForm = () => {
return (
<div>
<h2>Add New User</h2>
<Form method="post">
<input
type="text"
name="name"
id="name"
placeholder="Name"
required
/>
<input
type="email"
name="email"
id="email"
placeholder="Email"
required
/>
<button type="submit">Save</button>
</Form>
</div>
);
};
export default NewUserForm;
pages/ErrorPage.jsx
import { useRouteError } from "react-router-dom";
const ErrorPage = () => {
const error = useRouteError();
return (
<div style={{ padding: "20px", color: "red" }}>
<h2>⚠ Oops! Something went wrong</h2>
<p>{error.statusText || error.message}</p>
</div>
);
};
export default ErrorPage;
How It Works
- Visit
/users
→ Loader runs → Fetches users → Displays list. - Click Add New User → Navigate to
/users/new
. - Fill form → Action runs → Sends POST request.
- On success → Redirects back to
/users
with updated data. - On error → ErrorPage shows the error.
Run the App
# Start JSON Server
json-server --watch db.json --port 5000
# Start Vite
npm run dev
Now open http://localhost:5173/users 🎉
🎯 Conclusion
With Data APIs in React Router v6.4, data fetching and form submissions become more structured and declarative.
Pairing it with Vite and JSON Server makes it super easy to experiment and build small demos.
👉 If you’re learning React, try out this setup and explore how loaders, actions, and error boundaries simplify data management!
Top comments (0)