Engineering

React Routing and Form Libraries Compared

React routing and form libraries, compared. React Router, React Hook Form, Zod vs Yup, and react-paginate: real picks for real apps.

SA
Samer Alsayegh
Founder
Published
7 min read
Flat vector illustration of a React app split into a routing path with signpost icons and a form path with input fields and a checkmark, connected to a central React atom shape
Key takeaways

React Router and createBrowserRouter handle navigation, React Hook Form with a Zod resolver handles validation, and react-paginate or a table library handles pagination. None replace a typed content layer underneath.

React Router handles routing for most React apps. React Hook Form handles most forms. That's the short answer. The longer one is which flavor of each. BrowserRouter or createBrowserRouter. Zod or Yup. A pagination library or a hand-rolled one.

This guide picks through both, part of the wider framework and tooling landscape a React app sits inside. It also covers where a plain content API like Draftbase's removes a chunk of the leftover form-and-fetch work.

React Router: Still the Standard

react-router remains the default routing library for React apps. That's true for any app not using a framework's own router, like Next.js's App Router. As of version 7, the package is just react-router. react-router-dom still works for existing v6 projects. New projects should install react-router directly, per the official migration notes.

import { createBrowserRouter, RouterProvider } from "react-router";

const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "/blog/:slug", element: <Post /> },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

BrowserRouter vs createBrowserRouter

Two ways to wire up routing exist side by side. The older <BrowserRouter> plus <Routes> JSX style still works. It reads simply for a small app. createBrowserRouter is the newer, recommended path. It unlocks data loaders and actions. That means fetching data before a route renders, not after the component mounts and fires a useEffect.

For a page that fetches from a content API, that loader pattern matters. A blog post route can fetch its entry before rendering anything. No loading spinner flashes on every navigation.

Linking Between Pages

<Link> renders an anchor tag with no full page reload. It's the right default for any in-app navigation. useNavigate() covers what <Link> can't: redirecting after a form submits, or navigating from inside a useEffect. Reaching for useNavigate() on every click breaks something real. Keyboard navigation and middle-click-to-open-in-new-tab both need an actual anchor element.

React Hook Form: The Standard for Forms

React Hook Form won the forms space by keeping inputs uncontrolled by default. Most form libraries re-render the whole form on every keystroke. React Hook Form doesn't. It reads values through refs, not state.

import { useForm } from "react-hook-form";

function ContactForm() {
  const { register, handleSubmit } = useForm();
  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("email")} />
      <button type="submit">Send</button>
    </form>
  );
}

That's the whole form. No manual onChange handlers, no re-render per field. Validation plugs in separately, through a schema resolver.

React Hook Form with Zod vs Yup

Both plug into React Hook Form through the same @hookform/resolvers package. The integration quality is identical either way. The real difference is TypeScript.

Zod infers a static type straight from the schema, with z.infer<typeof schema>. Define the shape once. The same type flows into your form, your API handler, and your database call. Nothing needs syncing by hand. Zod has pulled ahead as the community default for TypeScript projects. It runs roughly 20M weekly downloads, against Yup's smaller share, per PkgPulse's 2026 comparison.

import { z } from "zod";

const schema = z.object({
  email: z.string().email(),
  title: z.string().min(3),
});
type FormData = z.infer<typeof schema>;

Yup still fits a plain JavaScript codebase. It also fits a team already deep in Yup schemas elsewhere. Its async validation support is real, and so is its gentler curve for non-TypeScript teams. Neither one decides the call for a new TypeScript project in 2026, though.

Pagination in React: react-paginate vs Rolling Your Own

react-paginate renders page numbers, a previous/next control, and an ellipsis break for large page counts. One component, one pageCount prop. It's the fastest path when a design needs standard numbered pagination and nothing more.

import ReactPaginate from "react-paginate";

<ReactPaginate
  pageCount={totalPages}
  onPageChange={({ selected }) => setPage(selected)}
  previousLabel="Prev"
  nextLabel="Next"
/>

Table in React: Pagination Plus Sorting

A data table usually needs more than page numbers alone. Sorting, column widths, and row selection tend to arrive in the same sprint. At that point, a table library like TanStack Table often replaces a standalone pagination component entirely. It ships its own usePagination logic alongside sorting and filtering, in one hook.

Cursor-based pagination is the other real option. It's more common on an API than in a UI component. A content delivery API built around cursors skips the "page 47 of 900" problem entirely. Each response carries a token for the next page, not an absolute page number. That number would shift anyway, as content changes underneath it.

The Real Cost These Libraries Don't Remove

React Router and React Hook Form both solve client-side problems. One navigates between routes. The other validates what a user types. Neither touches where the underlying data actually lives.

A route loader still needs something to fetch from. A form still needs somewhere to send validated data. Teams often reach for these libraries first. The database or CMS call gets bolted on after, typed loosely, and handled inconsistently across routes.

The fix is ordering the decision the other way. Pick the content or data layer first. Generate its types. Both the router's loaders and the form's Zod schema can reuse those same types, instead of hand-declaring a shape three times.

What Goes Wrong with Nested Routes and Forms?

A common failure shows up once routes nest more than one level deep. A parent route's loader fetches data, and a child route's own loader fetches more. Skip coordinating the two, and a slow child request blocks the whole page behind a spinner the parent route didn't need.

react-router's data APIs handle this through Await and deferred loading. The parent route renders right away. Only the slow part of the page waits. That pattern is easy to miss on a first pass, since a naive implementation just awaits everything up front and calls it done.

Forms have a matching trap. A form validated with Zod on the client still needs the same check on the server. Client-side validation is a UX layer, not a security wall. A user with dev tools open can submit anything past a client-only check. The Zod schema that checks the form should be the same schema that checks the API request body. Import it once, and reuse it. Don't redefine it on each side.

Do You Need react-router at All in a Next.js App?

No, and this trips up developers moving from a plain React app. Next.js's own App Router replaces react-router entirely: file-based routes, built-in <Link>, and its own data-fetching model through Server Components and fetch. Installing react-router inside a Next.js project usually means two competing routing systems fighting over the same URL bar. The React vs React Native comparison covers a related mismatch: React Navigation replaces react-router again once the app targets mobile instead of the browser.

The same question applies to React Hook Form, just less strictly. It still works fine inside Next.js, since form validation is a client concern regardless of the framework's router. The routing library is where the overlap actually breaks something.

When Do You Actually Need These Libraries?

A single-page app past two or three routes benefits from React Router's data APIs. Hand-rolled useState-based navigation stops scaling around there. A form past one or two fields benefits from React Hook Form too, mostly for the validation wiring, not the typing itself.

Below that size, both libraries add a dependency and a mental model. Three lines of useState already solve the same problem. A single search box or a one-field newsletter signup doesn't need a form library at all.

Is React Hook Form Hard to Learn?

Less than most form libraries, once one habit sinks in: register every input, don't manage its value by hand. Developers coming from a controlled-input background often keep writing useState for each field out of habit, then wire that state back into React Hook Form. That defeats the whole point.

The register function returns the props an input needs, name, onChange, onBlur, ref, spread directly onto the element. No local state, no manual wiring. The mental model flips once, and after that the library gets out of the way. Errors surface through formState.errors, keyed by field name, matching whatever shape the Zod or Yup schema defined.

Conclusion

React Router and createBrowserRouter handle navigation. React Hook Form, paired with Zod over Yup on a TypeScript project, handles validation. react-paginate or a table library handles pagination once a list gets long. None of them replace a typed data layer underneath. Draftbase's typed SDK generates the interfaces those forms and loaders both consume, straight from your content model.

Ship content that's built to be found

Draftbase generates schema, structured data, and a fast MDX editor for every post.

Frequently asked questions

Should I use react-router or react-router-dom?

Use react-router for a new project. As of version 7 it's the long-term package name. react-router-dom still works for an existing v6 app.

Is Zod better than Yup for React Hook Form?

For a TypeScript project, yes. Zod infers types straight from the schema, so one definition covers the form, the API, and the database. Yup needs those kept in sync by hand.

Does React Hook Form re-render on every keystroke?

No. It reads input values through refs, not state, so a keystroke in one field doesn't re-render the whole form.

What is react-paginate used for?

It renders page numbers, a next button, and a back button. One small piece, dropped in fast, with no full table setup needed.

Do I need react-router inside a Next.js app?

No. Next.js's App Router already handles routing through the file system. Adding react-router on top usually means two routers competing for the same URL.

SA
Samer Alsayegh
Founder at Draftbase

Samer is a software engineer and entrepreneur, founder of Draftbase and Ezi Home Services, building technology that simplifies home services. Passionate about software, APIs, automation, and creating products that solve real-world problems.

reactreact-routerreact-hook-formtypescript

Related posts

Draftbase is a headless CMS built for React devs.