Next.js has earned its place as one of the most popular frameworks in the React ecosystem. It gives you just about everything you need to build a modern web app with React—file-based routing, server-side rendering, static generation, and many other full-stack features.
Over time, Next.js has also become much more complex. Features like the App Router, Server Components, middleware, and streaming APIs have made it powerful but also more opinionated and less predictable. Additionally, Next.js (and Vercel, its parent company) has faced criticism lately, especially around vendor lock-in and the pace of change, leaving developers behind.
If you’re looking for a Next.js alternative and want to keep using React, you’re not short on options. In this article, we’ll cover the best frameworks to consider, what they offer, where they shine, and when they might be a better fit than Next.js.
Before we proceed with the article, here’s a high-level overview of the alternatives we’ll be reviewing:
Framework | Best For | React Support | SSR Support | Routing | Data Loading | Type Safety |
---|---|---|---|---|---|---|
Remix | Full-stack apps with built-in form/data primitives | Full | Yes | File-based + loaders/actions | Loaders + actions (built-in) | Yes |
Astro | Content-heavy static or hybrid sites | Partial | Yes (newer) | File-based with islands | JS fetch in Astro components | Yes |
TanStack Start | Fully type-safe full-stack React apps | Full | Yes | File-based via TanStack Router | Server functions + typed loaders | Yes (full stack) |
Vike | Full control over SSR/SSG with minimal abstraction | Full | Yes | Convention-based (+Page.tsx) | Custom server Hooks (onBeforeRender) | Yes |
Vite + React Router | Lightweight client-side React apps | Full | No (manual setup) | Manual via React Router | React Router loaders | Yes |
Remix is one of the strongest alternatives to Next.js, especially if you still want to stick with React but don’t like the direction Next.js is going.
Remix respects how the web works. It doesn’t try to abstract everything into framework magic; instead, it builds on native browser features like forms, caching, and HTTP requests and gives you a modern, React-first experience on top of that. It’s also completely platform-agnostic.
Let’s explore some of its core features and drawbacks below.
loader()
for data fetching and action()
for mutations like form submissions. These functions run server-side and are tightly scoped to the route itself, with no extra API layer requiredSimilar to Next.js, Remix uses a file-based routing system but with a more focused design. Each route is fully self-contained and handles its data fetching with a loader()
function and mutations with an action()
. There’s no need to fetch data at the top level or pass props down manually.
For example, here’s what a simple blog route looks like in Remix:
// app/routes/blog.tsx import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; export const loader = async () => { const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5"); if (!res.ok) throw new Response("Failed to fetch posts", { status: res.status }); const posts = await res.json(); return json({ posts }); }; export default function Blog() { const { posts } = useLoaderData<typeof loader>(); return ( <div> <h1>Latest Posts</h1> <ul> {posts.map((post) => ( <li key={post.id}> <strong>{post.title}</strong> <p>{post.body}</p> </li> ))} </ul> </div> ); }
This example blog page fetches data on the server using the loader()
and renders the posts without any client-side fetching or prop drilling.
Remix also lets you use standard React patterns if you do want client-side interactivity:
// app/routes/search.tsx import { useState } from "react"; export default function Search() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const handleSearch = async () => { setLoading(true); const res = await fetch(`https://jsonplaceholder.typicode.com/posts?q=${query}`); const data = await res.json(); setResults(data); setLoading(false); }; return ( <div> <h1>Search Posts</h1> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <button onClick={handleSearch} disabled={loading}> {loading ? "Searching..." : "Search"} </button> <ul> {results.map((post) => ( <li key={post.id}> <strong>{post.title}</strong> </li> ))} </ul> </div> ); }
Remix doesn’t get in the way of standard React. It just makes server-side logic simpler and more structured when you need it. You can check out our guide on Remix vs. Next.js vs. SvelteKit.
Astro isn’t explicitly a React framework. However, it integrates well with React, and depending on what you’re building, it might be a better fit than Next.js. Astro is built for content-heavy sites such as blogs, landing pages, documentation, etc., where performance matters more than full client-side interactivity. Its core strength is the island architecture, which renders everything as static HTML by default, and only the interactive parts of the page are hydrated with JavaScript. This way, you get tiny JS bundles and fast page loads.
As mentioned earlier, Astro pages are mostly HTML-first. You can drop in React (or any framework) components wherever interactivity is needed. Here’s a basic example of a blog page:
--- // src/pages/blog.astro import BlogList from "../components/BlogList.tsx"; const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5"); const posts = await response.json(); --- <html> <head><title>Blog</title></head> <body> <h1>Latest Posts</h1> <BlogList posts={posts} client:load /> </body> </html>
In this case, BlogList
is a React component that only gets hydrated on the client when needed, as shown below. The rest of the page is just static HTML:
// src/components/BlogList.tsx export default function BlogList({ posts }) { return ( <ul> {posts.map((post) => ( <li key={post.id}> <strong>{post.title}</strong> <p>{post.body}</p> </li> ))} </ul> ); }
You can also go full client-side inside your components when it makes sense. Astro stays out of the way and doesn’t try to control your whole app. You can also check out our article on how Astro compares to Next.js for React apps.
TanStack Start is a new full-stack React framework built by the team behind TanStack Query, Table, and Virtual. It’s not trying to reinvent the wheel; instead, it aims to make it easier to build fast, type-safe React apps using rock-solid tools. It supports SSR, streaming, server functions, and full-stack TypeScript without forcing a specific structure.
TanStack Start feels closer to building a pure React app than most frameworks. You’re not stuck in someone else’s opinionated folder structure, and there’s almost zero framework boilerplate. Routing is file-based but fully typed with TanStack Router, so navigation and data fetching are predictable and safe.
Here’s a basic route with server data fetching:
// src/routes/posts.tsx import { createRoute } from '@tanstack/react-router'; import { fetchPosts } from '../server/posts'; export const Route = createRoute({ path: '/posts', loader: async () => { return { posts: await fetchPosts(), }; }, component: PostsPage, }); function PostsPage({ posts }) { return ( <div> <h1>Blog Posts</h1> <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); }
And the server function could look like this:
// src/server/posts.ts import { db } from './db'; export async function fetchPosts() { return db.query('SELECT * FROM posts LIMIT 5'); }
With this setup, your logic lives where it makes sense, and everything is fully typed from front to back. It also doesn’t force you into black-box APIs.
Vike, (formerly vite-ssr) is a lightweight meta-framework for building React apps with full control over routing, server-side rendering, and static site generation. It’s built on top of Vite and designed to be unopinionated, i.e., no forced folder structures, no black-box abstractions, and no hidden behaviors.
If you’re coming from Next.js and feeling boxed in by conventions, Vike offers a more transparent and flexible alternative.
loader()
. You’re responsible for handling fetch logic, caching, and revalidation yourself. This gives you flexibility but adds setup overhead if you don’t already have a pattern in placeVike’s routing system is based on convention but without rigid folder rules. For a /blog page, you might have:
pages/ └── blog/ ├── +Page.tsx └── +Page.server.ts
With this structure, here’s what the server logic would look like:
// +Page.server.ts export async function onBeforeRender() { const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5"); const posts = await res.json(); return { pageContext: { pageProps: { posts }, }, }; }
And the corresponding component:
// +Page.tsx export default function Blog({ posts }: { posts: any[] }) { return ( <div> <h1>Latest Posts</h1> <ul> {posts.map((post) => ( <li key={post.id}> <strong>{post.title}</strong> </li> ))} </ul> </div> ); }
This works similarly to Remix’s loader()
, but everything is fully explicit. If you want control without compromise and you’re comfortable filling in the blanks yourself, Vike is a powerful alternative to Next.js.
Vite + React Router is another modern setup that many developers are sleeping on. While it’s not branded as a framework, it gives you everything you need to build fast, maintainable React apps without the overhead. Vite handles your build tooling with speed and precision, and React Router gives you flexible routing with support for nested routes, loaders, and data fetching.
Here’s a basic setup using React Router’s useLoaderData()
API for route-level data fetching:
// main.tsx import { createRoot } from 'react-dom/client'; import { RouterProvider, createBrowserRouter, } from 'react-router-dom'; import routes from './routes'; const router = createBrowserRouter(routes); createRoot(document.getElementById('root')!).render( <RouterProvider router={router} /> ); // routes.tsx import Home from './pages/Home'; import Blog from './pages/Blog'; export default [ { path: '/', element: <Home />, }, { path: '/blog', element: <Blog />, loader: async () => { const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5'); if (!res.ok) throw new Response('Failed to load', { status: res.status }); return res.json(); }, }, ];
And then the page itself:
// pages/Blog.tsx import { useLoaderData } from 'react-router-dom'; export default function Blog() { const posts = useLoaderData() as any[]; return ( <div> <h1>Latest Posts</h1> <ul> {posts.map((post) => ( <li key={post.id}> <strong>{post.title}</strong> </li> ))} </ul> </div> ); }
This setup gives you declarative, route-scoped data loading, similar to Next.js or Remix, without adopting a larger framework.
Picking the right framework/setup mostly depends on what you’re building and how much control, performance, or simplicity you want.
Here’s how to decide which one makes sense for your project:
Next.js is still a powerful framework, but it’s no longer the only option for building React apps. If you’re feeling boxed in by Vercel’s ecosystem or overwhelmed by the constant API changes, you’re not alone. This article highlights some of the top alternatives worth exploring if you want more control over your stack.
Thanks for reading!
Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket captures console logs, errors, network requests, and pixel-perfect DOM recordings from user sessions and lets you replay them as users saw it, eliminating guesswork around why bugs happen — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowHow does TanStack Form, a newer form library, compare to React Hook Form, and should you consider using it?
A deep dive into the Liskov Substitution Principle with examples, violations, and practical tips for writing scalable, bug-free object-oriented code.
This article walks through new CSS features like ::scroll-button()
and ::scroll-marker()
that make it possible to build fully functional CSS-only carousels.
Let’s talk about one of the greatest problems in software development: nascent developers bouncing off grouchy superiors into the arms of AI.