Back to the Web: Why React Router Feels Different
Remember when building a website was just some HTML, a few <a href="/">
links, and you were good to go?
Over the years, we added a lot of tools and layers to make our apps more powerful. But in the process, routing got more complicated. We started writing big config files, using nested JavaScript objects, and routing felt like something separate from the rest of the app.
Lately, though, things have been shifting. Next.js helped bring server-side rendering back into focus. Remix took it even further, showing how forms, links, and layouts could work with the web instead of around it.
Now React Router picks up on those ideas. It lets you write routes using JSX or files — and lets your routing match the structure of your app. No more jumping between config and UI or being forced into a specific layout setup.
You also get features like nested layouts, data loaders, and actions — all without needing a full-stack framework. Just plain React, with better routing.
In this article, we’ll look at what’s new in React Router, where these ideas come from, and how it helps us build apps that feel a bit more like the web we started with — simple, clear, and built on HTML.
Learn the Web First
One thing that really stands out with React Router is how much it respects the web. It doesn’t try to hide the platform — it works with it. You’re using real links, real forms, and real browser behavior. And because it's just React, you stay in control of how far you want to go.
That’s a big contrast with something like Next.js. The first thing you're taught about data loading there is Server Components. Powerful? Absolutely. But also abstract, and pretty far removed from how the web actually works under the hood.
That’s why I think it’s so valuable to first learn JavaScript and the web itself. Understand routing, navigation, browser behavior, and HTTP. Then when a framework makes a decision — like hiding client-side transitions or baking in a cache layer — you’ll know why. React Router supports that kind of learning. It keeps you close to the platform and lets you gradually layer in advanced patterns, rather than forcing you into a “new way” from the start.
Three Modes, One Philosophy
React Router gives you three runtime modes to choose from: Declarative, Data, and Framework.
You can start simple by defining routes in JSX (declarative mode), scale up with route objects and loaders (data mode), or — and here’s where it gets exciting — adopt framework mode, where your routes are defined through a file-based structure that feels a lot like traditional HTML routing.
In framework mode, your route config might look like this:
import { index, route } from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("products/:pid", "./product.tsx"),
];
This isn’t magic. You’re explicitly defining paths and pointing to the components that should handle them — just like a classic web server mapping URLs to files. The difference? Each file can also export a loader
, action
, or error boundary
, giving you full control over data and behavior per route.
It’s this structure — simple, readable, and colocated — that makes framework mode such a joy to work with. And it’s what brings us to one of React Router’s most powerful features: loaders and actions.
Loaders and Actions: Like the Old Web, But Better
One of the most refreshing things about React Router — especially in framework mode — is how familiar it feels if you’ve ever built websites in the pre-SPA era.
🔄 loader()
: like an old-school GET request
export async function loader({ params }: Route.LoaderArgs) {
let product = await getProduct(params.pid);
return { product };
}
📝 action()
: like a classic POST
export async function action({ request }: Route.ActionArgs) {
let formData = await request.formData();
await saveProduct(formData);
return redirect("/products");
}
✅ Validating Forms with Zod (on the Server)
import { z } from "zod";
import { json, redirect } from "react-router-dom";
const SignupSchema = z.object({
email: z.string().email("Please enter a valid email."),
password: z.string().min(8, "Password must be at least 8 characters."),
});
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const data = Object.fromEntries(formData);
const result = SignupSchema.safeParse(data);
if (!result.success) {
const errors = result.error.flatten().fieldErrors;
return json({ errors }, { status: 400 });
}
const { email, password } = result.data;
await createUser(email, password);
return redirect("/welcome");
}
import { useActionData, Form } from "react-router-dom";
export default function Signup() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<label>
Email:
<input name="email" />
{actionData?.errors?.email &amp;&amp; (
<p>{actionData.errors.email[0]}</p>
)}
</label>
<label>
Password:
<input name="password" type="password" />
{actionData?.errors?.password &amp;&amp; (
<p>{actionData.errors.password[0]}</p>
)}
</label>
<button type="submit">Sign up</button>
</Form>
);
}
HTML All the Way Down
React Router doesn’t just let you simulate the web — it works with it.
- You use
<a href>
for navigation. - You use real
<form>
tags to submit data. - You colocate data and logic with the component that needs it.
- You render what you load, and load what you render.
Conclusion
React Router isn’t just “still around.” It’s better than ever — and in my opinion, it’s the best way forward for building web apps in React today.
It brings back everything that made the web great in the first place. No black box. No framework lock-in. Just the web, done right.
If you're serious about React, and you want to build apps that are fast, flexible, and web-native, React Router is where it's at.
Welcome back to the web.