Blog Update: Upgrading to React Router v7 and React v19
2025-04-26My experience upgrading this blog from Remix v2 to React Router v7 and React v18 to v19, with insights on streaming, migration strategies, and the philosophy behind these modern web technologies.
A few weeks ago, I shared my journey of one year with Remix after migrating from Gatsby. Today, I'm excited to write about another upgrade I've just completed: moving to React Router v7 and React v19. This upgrade has brought some significant improvements to how this blog operates, particularly in terms of performance and developer experience.
The Philosophy of React Router v7
React Router v7 represents a significant shift in how we think about routing in React applications. Its core philosophy aligns perfectly with how Remix has been evolving.
Server-first mindset is it's basic mindset, meaning prioritizing server rendering and progressive enhancement. Remix also embodies streaming by default - Remix breaks the request-response cycle into smaller, more manageable chunks, which is innovative and creative in my opinion.
The most exciting aspect is how these tools are converging around a common set of patterns. React Router v7 now incorporates many of the concepts that made Remix so powerful, while Remix itself has evolved to leverage these new capabilities.
Streaming: A Game Changer for User Experience
One of the most impactful changes in this upgrade was fully embracing streaming. While Remix had initial support for this feature, React Router v7 and React v19 take it to another level.
On the index page of this blog, I'm now using streaming to show featured posts immediately while deferring the loading of the complete post list:
- Load with await: Featured posts load first, providing immediate value to visitors
- Load async function without await: The rest of the content streams in progressively as it becomes available
- Outcome: Users can interact with the page even before all data has finished loading
This pattern significantly improves perceived performance. Visitors don't have to wait for all data to be fetched before they see something useful on the screen.
The loader implementation looks like this:
export async function loader({ params }) {
return data(
{
// Data that can be returned immediately
featuredPosts: await getPostsFeatured({ postType: params.postType }),
// Data that will be streamed later
posts: getPosts({ postType: params.postType }),
}
);
}
The component logics look simple and elegant:
export default function Posts() {
const { posts, featuredPosts } = useLoaderData<typeof loader>();
return (
<RootLayout>
{/* Featured posts render immediately */}
{featuredPosts && featuredPosts.length > 0 && (
<PostCarousel posts={featuredPosts} title="Featured Posts" />
)}
{/* Regular posts stream in with Suspense boundary */}
<Suspense fallback={<p>Loading posts...</p>}>
<Await resolve={posts} errorElement={<p>Error loading posts!</p>} >
{resolvedPosts => <PostFeed posts={resolvedPosts} />}
</Await>
</Suspense>
</RootLayout>
);
}
This approach delivers the best of both worlds: fast initial page loads with progressively enhanced content. I'm expanding this pattern further, for example showing recommended pages lazily in each blog post pages.
Smooth Migration Experience
When I first heard about these major version upgrades, I was concerned about the potential complexity. However, the migration process was surprisingly smooth, thanks to:
- Future flags in Remix v2
- Helpful codemods:
npx codemod remix/2/react-router/upgrade
- Clear documentation
The migration path depends on your current source code and configuration. In my case, fortunately the whole migration did not require much works to be honest. Here is the non-exhaustive list of what I've done during this migration (writing this out as far as I remember):
- Updating imports from
@remix-run/cloudflare
to@react-router/cloudflare
- Updating NPM scripts
- Adding
routes.ts
files and few other configurations - Using the new
data()
function for response handling, instead ofjson() / defer()
- Introduce
fs-routes
to keep using the Remis-based routing configuration (which I later replaced with routes.tsx as I usually do not like having many dependencies inpackage.json
)
Improvements Over the Last Year
One of my favorite aspects of maintaining this blog is the opportunity to invest in emerging technologies. This upgrade is a perfect example of that philosophy.
Staying at the forefront of these technologies allows me to bring these insights into my other projects, creating a virtuous cycle of learning and improvement.
Comparing where the blog is now to where it was a year ago and also how I migrated away from Gatsby to Remix, the evolution is clear:
Aspect | Few Years Ago | One Year Ago | Current Implementation |
---|---|---|---|
Framework | Gatsby | Remix v2 | Remix with React Router v7 |
Rendering Strategy | Fully pre-rendered HTML | Server-side with minimal streaming | Full streaming with Suspense |
Data Loading | Build-time only | Primarily build-time processing | Hybrid with more dynamic capabilities |
Build Process | Long build times | Faster incremental builds | Optimized build pipeline |
React Version | React 17 | React 18 | React 19 |
Router Implementation | Gatsby Router | Remix conventional routing | React Router v7 alignment |
These changes have resulted in even better performance metrics and a more maintainable codebase.
Clean State
During the migration, I already upgraded all other dependencies to the latest. What a satisfying output!
$ npm outdated # show no output, meaning all dependencies are up-to-date
$ npm audit
found 0 vulnerabilities
Conclusion
Upgrading to React Router v7 and React v19 has been a simple yet rewarding experience. The migration was smoother than expected, and the benefits in terms of performance and developer experience have been great.