React Frameworks (Next.js, Remix)
React is intentionally a library, not a framework. It handles the view layer brilliantly but ships without routing, server rendering, build optimisation, or data fetching conventions. For production apps you almost always layer a framework on top. This page surveys the major options and explains when each one wins.
Why Use a Framework at All?
Routing — frameworks provide file-system or convention-based routing so you don't configure react-router from scratch
Server-side rendering (SSR) / Static generation (SSG) — critical for SEO and initial page-load performance
Build optimisation — automatic code splitting, image and font optimisation, tree shaking
Full-stack capability — API routes, server actions, and middleware colocated with your UI code
Deployment integration — one-command deploys to Vercel, Netlify, Cloudflare, or your own server
Developer experience — fast refresh, TypeScript preset, sensible defaults out of the box
Next.js — The Dominant Choice
Next.js (maintained by Vercel) is by far the most widely adopted React framework. It supports two routing systems: the App Router (Next.js 13+, the future) and the Pages Router (stable, battle-tested). New projects should use the App Router.
App Router highlights:
File-based routing under
app/— every folder is a route segmentReact Server Components by default — fetch data directly in components
Nested layouts with
layout.tsx— shell UI shared across child routes without re-renderingServer Actions — async functions that run on the server, callable from forms and event handlers
Streaming with Suspense — stream HTML chunks as data resolves
Route handlers (
route.ts) replace API routes
// app/blog/[slug]/page.tsx — App Router page
interface Props {
params: { slug: string }
}
// generateStaticParams → pre-render at build time (SSG)
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((r) => r.json())
return posts.map((p: { slug: string }) => ({ slug: p.slug }))
}
export default async function BlogPost({ params }: Props) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(
(r) => r.json()
)
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
)
}Pages Router (legacy but still fully supported):
// pages/blog/[slug].tsx — Pages Router page
import type { GetStaticProps, GetStaticPaths } from 'next'
interface Props {
post: { title: string; body: string }
}
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await fetch('https://api.example.com/posts').then((r) => r.json())
return {
paths: posts.map((p: { slug: string }) => ({ params: { slug: p.slug } })),
fallback: false,
}
}
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
const post = await fetch(`https://api.example.com/posts/${params!.slug}`).then(
(r) => r.json()
)
return { props: { post }, revalidate: 60 }
}
export default function BlogPost({ post }: Props) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
)
}Next.js also provides first-class optimisation components:
import Image from 'next/image'
import Link from 'next/link'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] }) // font loaded zero-layout-shift
export function Header() {
return (
<header className={inter.className}>
<Link href="/">Home</Link>
<Image
src="/logo.png"
alt="Company logo"
width={120}
height={40}
priority // preload as LCP candidate
/>
</header>
)
}Remix — Web Standards First
Remix (now part of the React Router ecosystem under Shopify) takes a different philosophy: embrace web platform fundamentals — HTML forms, HTTP semantics, browser navigation — rather than recreate them in JavaScript. The result is apps that degrade gracefully and often perform better with less code.
The core pattern is loaders (data for GET) and actions (data mutations for POST/PUT/DELETE), colocated with their route:
// app/routes/contacts.$contactId.tsx — Remix route
import { json, redirect } from '@remix-run/node'
import { useLoaderData, Form } from '@remix-run/react'
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node'
import { getContact, updateContact } from '~/data'
// loader runs on the server for GET requests
export async function loader({ params }: LoaderFunctionArgs) {
const contact = await getContact(params.contactId)
if (!contact) throw new Response('Not Found', { status: 404 })
return json({ contact })
}
// action runs on the server for form submissions
export async function action({ params, request }: ActionFunctionArgs) {
const formData = await request.formData()
await updateContact(params.contactId, Object.fromEntries(formData))
return redirect(`/contacts/${params.contactId}`)
}
export default function ContactDetail() {
const { contact } = useLoaderData<typeof loader>()
return (
<Form method="post">
<input name="name" defaultValue={contact.name} />
<button type="submit">Save</button>
</Form>
)
}Key Remix differentiators:
Nested routes — each route segment can have its own loader, action, and error boundary. Errors are isolated to the broken segment
Progressive enhancement — forms work without JavaScript enabled; JS adds optimistic UI on top
No client-side data cache — data is always fresh from the server (loaders re-run on navigation)
Edge-first — runs natively on Cloudflare Workers, Deno Deploy, and other edge runtimes
Gatsby — Static Site Specialist
Gatsby excels at content-heavy static sites: marketing pages, blogs, documentation, e-commerce catalogues. It builds every page at compile time and ships a highly optimised static bundle. Its GraphQL data layer lets you pull from CMSes (Contentful, Sanity, WordPress), markdown files, and APIs into a unified query interface.
// src/pages/blog/{markdownRemark.frontmatter__slug}.tsx
import { graphql, type PageProps } from 'gatsby'
export const query = graphql`
query BlogPost($id: String!) {
markdownRemark(id: { eq: $id }) {
html
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
}
}
}
`
export default function BlogPost({ data }: PageProps<Queries.BlogPostQuery>) {
const post = data.markdownRemark!
return (
<main>
<h1>{post.frontmatter?.title}</h1>
<time>{post.frontmatter?.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.html! }} />
</main>
)
}Gatsby has lost significant mindshare to Next.js for general-purpose projects but remains the best choice when GraphQL-driven static generation and Gatsby's plugin ecosystem (image processing, CMS connectors) are priorities.
Expo / React Native — Mobile
React Native lets you write React components that compile to native iOS and Android views — not a WebView. Expo wraps React Native with a managed workflow, OTA updates, and a massive library of native modules. With Expo Router, you get file-based routing that works across iOS, Android, and the web from a single codebase.
// app/(tabs)/profile.tsx — Expo Router screen
import { View, Text, StyleSheet } from 'react-native'
import { useLocalSearchParams } from 'expo-router'
export default function ProfileScreen() {
const { userId } = useLocalSearchParams<{ userId: string }>()
return (
<View style={styles.container}>
<Text style={styles.title}>User {userId}</Text>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
title: { fontSize: 24, fontWeight: 'bold' },
})Framework Comparison
Framework | Best for | Rendering | Key strength |
|---|---|---|---|
Next.js (App Router) | Full-stack web apps | SSR, SSG, RSC, ISR | RSC, server actions, Vercel platform |
Next.js (Pages Router) | Existing Next.js apps | SSR, SSG, ISR | Stability, battle-tested |
Remix / React Router v7 | Data-heavy, form-driven apps | SSR | Web standards, nested routes, edge |
Gatsby | Marketing sites, blogs | SSG | GraphQL data layer, plugin ecosystem |
Expo | iOS / Android / web | Native + web | Shared codebase across platforms |
Vite SPA (no framework) | SPAs, dashboards, internal tools | CSR | Simplicity, no server needed |
When to Use Bare React (Vite SPA)
Not every project needs a framework. A Vite-powered SPA is the right choice when:
The app lives behind authentication (SEO is irrelevant — Google doesn't index your dashboard)
You're building an internal tool, admin panel, or data visualisation
The backend is owned by another team and React is purely a UI layer
You want maximum flexibility with no framework opinions on routing or data
Build-time complexity needs to be minimal (no server to manage)