Blog Post

React Router v7: Next.js Power for Plain React

September 2025

I've been using React Router v7 on a recent project and it honestly feels like the power of Next.js, but for plain React.

Here's what stood out:

File-Based Routing (Optional)

Organize routes by folder/files, or use a routes config if you prefer. It's flexible—choose what works for your project.

// File-based routing
app/
  routes/
    users/
      $userId.tsx  // /users/:userId
      index.tsx    // /users

Nested Layouts

Compose layouts at each route level (headers, sidebars) and let children inherit them. This creates a cleaner structure without prop drilling.

// Parent layout
function DashboardLayout() {
  return (
    <div>
      <Sidebar />
      <Outlet /> {/* Child routes render here */}
    </div>
  );
}

Loaders and Actions: The Game Changer

Before v7:

Each page fetched its data manually (usually with useEffect). Result: loading flashes and duplicated fetch logic everywhere.

Now with v7:

React Router knows ahead of time what data each route needs:

  • Loader: Fetches data before render
  • Action: Handles form submissions

No more messy useEffects. No more scattered fetch calls. Clean, predictable data right at the route level.

Here's how it works:

// Define loader - runs before component renders
export async function loader({ params }: LoaderFunctionArgs) {
  const user = await fetchUser(params.userId);
  return { user };
}

// Component receives data immediately
export default function UserProfile() {
  const { user } = useLoaderData<typeof loader>();
  
  // No loading state needed - data is already here
  return <div>{user.name}</div>;
}

Why Performance and Flexibility Improve

  • Faster renders: Data and UI load together
  • Fewer re-renders: Data fetched once at route level
  • Perfect for dashboards or SPAs that don't need SSR

Loader vs useEffect

Loader: For one-time fetches tied to the route (e.g., /users/:id). Runs before the component renders.

useEffect: For side effects or live behavior after the page loads, like timers, analytics, or real-time updates.

Loaders = first paint with data

useEffect = after the paint

For Live Dashboards

Loaders fetch once when the route loads. For ongoing updates, use:

  • useEffect + WebSockets/SSE, or
  • Realtime hooks from tools like Convex (useQuery) or Supabase Realtime
// Loader fetches initial data
export async function loader() {
  return await fetchInitialData();
}

// Component uses realtime for updates
export default function Dashboard() {
  const initialData = useLoaderData();
  const liveData = useQuery(api.getLiveData); // Convex realtime
  
  return <div>...</div>;
}

The Takeaway

React Router v7 brings Next.js-like patterns to plain React apps. File-based routing, nested layouts, and loaders eliminate the useEffect fetch pattern, making data flow predictable and performance better.

For SPAs and dashboards that don't need SSR, it's a solid choice.

Will share my experience of using Convex as a complete backend service soon.

Rajea Bilal | AI Developer