DEV Community

Cover image for React Router v6.4 Data APIs Tutorial
Sanjay Singh
Sanjay Singh

Posted on

React Router v6.4 Data APIs Tutorial

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
Enter fullscreen mode Exit fullscreen mode

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"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Run JSON Server:

json-server --watch db.json --port 5000
Enter fullscreen mode Exit fullscreen mode

Project Structure

src/
 ├── App.jsx
 ├── main.jsx
 ├── router.js
 ├── pages/
 │    ├── Users.jsx
 │    ├── NewUserForm.jsx
 │    └── ErrorPage.jsx
 └── db.json
Enter fullscreen mode Exit fullscreen mode

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} />
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

How It Works

  1. Visit /usersLoader runs → Fetches users → Displays list.
  2. Click Add New User → Navigate to /users/new.
  3. Fill form → Action runs → Sends POST request.
  4. On success → Redirects back to /users with updated data.
  5. On error → ErrorPage shows the error.

Run the App

# Start JSON Server
json-server --watch db.json --port 5000

# Start Vite
npm run dev
Enter fullscreen mode Exit fullscreen mode

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)