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.