10 Key Differences Between React.js and Next.js in 2025
Explore the evolving landscape of React.js and Next.js in 2025.

React.js and Next.js serve different purposes in the modern web development ecosystem. This guide highlights the 10 most important differences between these technologies in 2025, helping you make an informed decision for your next project.
Rendering Approaches#
React.js: In 2025, React continues to focus primarily on client-side rendering (CSR) out of the box. While React 19's improved server components have enhanced its server capabilities, implementing server-side rendering (SSR) still requires additional configuration and tooling. Next.js: Next.js 14+ offers a unified rendering framework with multiple built-in rendering strategies:• Server Components (default)
• Client Components (with 'use client' directive)
• Static Site Generation (SSG)
• Incremental Static Regeneration (ISR)
• Dynamic rendering with streaming
The App Router introduced in Next.js 13 and refined through version 14 has become the standard, offering more intuitive control over rendering patterns.
React.js Example: Client-Side Rendering// React.js Client-Side Rendering
import { useState, useEffect } from 'react';
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Data fetching happens in the browser
async function fetchProduct() {
try {
const response = await fetch(`/api/products/${productId}`);
const data = await response.json();
setProduct(data);
} catch (error) {
console.error('Failed to fetch product:', error);
} finally {
setLoading(false);
}
}
fetchProduct();
}, [productId]);
if (loading) return <div>Loading...</div>;
if (!product) return <div>Product not found</div>;
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<button onClick={() => console.log('Add to cart')}>Add to Cart</button>
</div>
);
}// Next.js Server Component (default in App Router)
// app/products/[id]/page.js
import { ProductActions } from './product-actions';
// This component runs on the server
export default async function ProductPage({ params }) {
// Data fetching happens on the server
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 3600 }, // Revalidate every hour
}).then(res => res.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ProductActions product={product} />
</div>
);
}
// app/products/[id]/product-actions.js
('use client');
import { useState } from 'react';
// This component runs on the client
export function ProductActions({ product }) {
const [inCart, setInCart] = useState(false);
return (
<button
onClick={() => setInCart(!inCart)}
className={inCart ? 'bg-green-500' : 'bg-blue-500'}
>
{inCart ? 'Remove from Cart' : 'Add to Cart'}
</button>
);
}Routing System#
React.js: React still requires third-party libraries like React Router for routing. The latest React Router v7 offers improved features, but setting up routing, nested routes, and route protection remains the developer's responsibility. Next.js: The App Router in Next.js provides a file-system based routing approach that's both intuitive and powerful:• Nested routes through folder structure
• Dynamic segments with [bracket] notation
• Route groups with (parentheses)
• Parallel routes with @folder notation
• Intercepting routes for modals and overlays
• Built-in middleware for authentication and redirects
React.js Example: React Router Setup// React.js with React Router v7
import {
createBrowserRouter,
RouterProvider,
Route,
createRoutesFromElements,
Outlet,
useParams,
Link,
} from 'react-router-dom';
// Root layout component
function RootLayout() {
return (
<div>
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/products">Products</Link>
<Link to="/blog">Blog</Link>
</nav>
</header>
<main>
{/<em> Nested routes render here </em>/}
<Outlet />
</main>
<footer> 2025 My Store</footer>
</div>
);
}
// Product list page
function Products() {
return (
<div>
<h1>Products</h1>
<ul>
<li>
<Link to="/products/1">Product 1</Link>
</li>
<li>
<Link to="/products/2">Product 2</Link>
</li>
<li>
<Link to="/products/3">Product 3</Link>
</li>
</ul>
<Outlet /> {/<em> For nested product routes </em>/}
</div>
);
}
// Individual product page
function Product() {
const { productId } = useParams();
return <div>Product Details for ID: {productId}</div>;
}
// Create router with routes
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<RootLayout />}>
<Route index element={<HomePage />} />
<Route path="products" element={<Products />}>
<Route index element={<ProductsIndex />} />
<Route path=":productId" element={<Product />} />
</Route>
<Route path="blog" element={<Blog />} />
<Route path="<em>" element={<NotFound />} />
</Route>
)
);
// Render the router
function App() {
return <RouterProvider router={router} />;
}In Next.js, routing is handled through the file system structure. Here's how the same routing would be implemented:
app/
├── layout.js # RootLayout component
├── page.js # HomePage component
├── products/
│ ├── page.js # ProductsIndex component
│ ├── layout.js # Products layout with navigation
│ └── [productId]/
│ └── page.js # Product component
└── blog/
└── page.js # Blog component// app/layout.js - Root layout
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/blog">Blog</a>
</nav>
</header>
<main>{children}</main>
<footer> 2025 My Store</footer>
</body>
</html>
);
}
// app/products/layout.js - Products layout
export default function ProductsLayout({ children }) {
return (
<div>
<h1>Products</h1>
<ul>
<li><a href="/products/1">Product 1</a></li>
<li><a href="/products/2">Product 2</a></li>
<li><a href="/products/3">Product 3</a></li>
</ul>
{children}
</div>
);
}
// app/products/[productId]/page.js - Product details
export default function Product({ params }) {
return <div>Product Details for ID: {params.productId}</div>;
}Next.js offers several advanced routing patterns that would require significant custom code in React Router:
Parallel RoutesParallel routes allow you to simultaneously show multiple pages in the same view:
// app/dashboard/@stats/page.js
export default function Stats() {
return <div>Statistics Panel</div>;
}
// app/dashboard/@activity/page.js
export default function Activity() {
return <div>Recent Activity</div>;
}
// app/dashboard/layout.js
export default function DashboardLayout({ stats, activity, children }) {
return (
<div className="dashboard-grid">
<div className="main-content">{children}</div>
<div className="stats-panel">{stats}</div>
<div className="activity-feed">{activity}</div>
</div>
);
}Intercepting routes are perfect for modal patterns where you want to show content without losing the current page context:
app/
├── products/
│ ├── page.js # Products list
│ └── [id]/
│ └── page.js # Product details page
└── @modal/
└── products/
└── [id]/
└── page.js # Product modal viewRoute groups let you organize routes without affecting the URL structure:
app/
├── (shop)/ # Route group (doesn't affect URL)
│ ├── products/
│ │ └── page.js # /products
│ └── categories/
│ └── page.js # /categories
└── (marketing)/
├── blog/
│ └── page.js # /blog
└── about/
└── page.js # /aboutuse hook and improved Suspense, but data fetching patterns still require custom implementation or third-party libraries like React Query or SWR.
Next.js: Next.js offers multiple built-in data fetching methods:
• Server Components with async/await
• The fetch API with automatic deduplication
• Revalidation strategies (time-based, on-demand)
• Advanced caching mechanisms
React.js Example: Data Fetching with React Query// React.js with React Query
import {
useQuery,
useMutation,
QueryClient,
QueryClientProvider,
} from 'react-query';
// Create a client
const queryClient = new QueryClient();
// Wrap your app with QueryClientProvider
function App() {
return (
<QueryClientProvider client={queryClient}>
<ProductList />
</QueryClientProvider>
);
}
// Component with data fetching
function ProductList() {
// Fetch products
const {
data: products,
isLoading,
error,
} = useQuery(
'products',
async () => {
const response = await fetch('/api/products');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
},
{
staleTime: 60000, // Consider data fresh for 1 minute
refetchOnWindowFocus: true, // Refetch when window regains focus
}
);
// Mutation for adding a product
const addProduct = useMutation(
async newProduct => {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProduct),
});
return response.json();
},
{
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries('products');
},
}
);
if (isLoading) return <div>Loading products...</div>;
if (error) return <div>Error loading products: {error.message}</div>;
return (
<div>
<h1>Products</h1>
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
<button
onClick={() => {
addProduct.mutate({ name: 'New Product', price: 9.99 });
}}
>
Add Product
</button>
</div>
);
}// app/products/page.js
import { AddProductForm } from './add-product-form';
import { revalidatePath } from 'next/cache';
// Server action for adding a product
async function addProduct(formData) {
'use server';
const name = formData.get('name');
const price = formData.get('price');
await fetch('https://api.example.com/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, price }),
});
// Revalidate the products page
revalidatePath('/products');
}
// Server Component with data fetching
export default async function ProductList() {
// This fetch is automatically memoized and deduplicated
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }, // Revalidate every 60 seconds
}).then(res => res.json());
return (
<div>
<h1>Products</h1>
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
<AddProductForm addProduct={addProduct} />
</div>
);
}
// app/products/add-product-form.js
('use client');
export function AddProductForm({ addProduct }) {
return (
<form action={addProduct}>
<input name="name" placeholder="Product name" required />
<input
name="price"
type="number"
step="0.01"
placeholder="Price"
required
/>
<button type="submit">Add Product</button>
</form>
);
}Next.js offers several data fetching patterns that are difficult to implement in plain React:
Parallel Data Fetching// Fetch multiple resources in parallel
export default async function Dashboard() {
// These requests run in parallel
const [userData, orderData, productData] = await Promise.all([
fetch('https://api.example.com/user').then(res => res.json()),
fetch('https://api.example.com/orders').then(res => res.json()),
fetch('https://api.example.com/products').then(res => res.json()),
]);
return (
<div>
<UserProfile user={userData} />
<RecentOrders orders={orderData} />
<PopularProducts products={productData} />
</div>
);
}import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
{/</em> This loads immediately <em>/}
<DashboardHeader />
{/</em> This streams in when ready <em>/}
<Suspense fallback={<OrdersSkeleton />}>
<OrdersTable />
</Suspense>
{/</em> This streams in when ready <em>/}
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
</div>
);
}
// These components can have their own async data fetching
async function OrdersTable() {
const orders = await fetch('https://api.example.com/orders').then(res =>
res.json()
);
return <table>{/</em> Render orders <em>/}</table>;
}
async function RevenueChart() {
const revenue = await fetch('https://api.example.com/revenue').then(res =>
res.json()
);
return <div>{/</em> Render chart <em>/}</div>;
}Build Optimization#
React.js: React requires additional tooling like Webpack or Vite for build optimization. While these tools are powerful, configuring them for optimal performance requires expertise. Next.js: Next.js includes built-in optimizations:• Automatic code splitting
• Tree shaking
• Image optimization with next/image
• Font optimization with next/font
• Script optimization with next/script
• Turbopack integration for faster builds (now stable in 2025)
React.js Example: Manual Build OptimizationWith React, you need to manually configure build tools. Here's an example using Vite:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
import { splitVendorChunkPlugin } from 'vite';
import compression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
react(),
splitVendorChunkPlugin(), // Split vendor chunks
compression(), // Compress assets
visualizer({
// Analyze bundle size
open: true,
gzipSize: true,
brotliSize: true,
}),
],
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
},
},
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-vendor': ['react-bootstrap', '@mui/material'],
utils: ['lodash', 'date-fns'],
},
},
},
},
optimizeDeps: {
include: ['react', 'react-dom'],
},
});For image optimization, you would need to implement custom components:
// ImageOptimizer.jsx
import { useState, useEffect } from 'react';
function ImageOptimizer({ src, width, height, alt, ...props }) {
const [imageSrc, setImageSrc] = useState('');
useEffect(() => {
// Generate responsive image URLs
const generateOptimizedUrl = (url, width) => {
// This is a simplified example - in reality, you'd use an image service
return `https://image-optimizer.example.com?url=${encodeURIComponent(url)}&width=${width}`;
};
setImageSrc(generateOptimizedUrl(src, width));
}, [src, width]);
return (
<img
src={imageSrc || src}
width={width}
height={height}
alt={alt}
loading="lazy"
{...props}
/>
);
}Next.js provides optimizations out of the box:
// app/products/[id]/page.js
import Image from 'next/image';
import { Inter, Roboto } from 'next/font/google';
import Script from 'next/script';
// Font optimization with automatic subset
const inter = Inter({ subsets: ['latin'] });
const roboto = Roboto({
weight: ['400', '700'],
subsets: ['latin'],
display: 'swap',
});
export default function ProductPage({ params }) {
return (
<div className={inter.className}>
<h1 className={roboto.className}>Product Details</h1>
{/</em> Optimized image with automatic WebP/AVIF conversion <em>/}
<Image
src={`/products/${params.id}.jpg`}
width={600}
height={400}
alt="Product image"
priority
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
{/</em> Optimized third-party script loading <em>/}
<Script
src="https://analytics.example.com/script.js"
strategy="lazyOnload"
onLoad={() => console.log('Analytics script loaded')}
/>
{/</em> Product content <em>/}
<div>Product ID: {params.id}</div>
</div>
);
}Next.js also provides a built-in performance analysis tool:
# Build with bundle analysis
npx next build --analyzeAPI Routes and Backend Integration#
React.js: React is strictly a frontend library. For API endpoints, you need a separate backend server or serverless functions. Next.js: Next.js offers built-in API routes through:• Route Handlers in the App Router
• Serverless functions that deploy automatically
• Edge Runtime support for global low-latency functions
• Direct database access from Server Components
Advanced Next.js API FeaturesNext.js offers several advanced API patterns:
Edge API Routes// app/api/geo/route.js
import { NextResponse } from 'next/server';
export const runtime = 'edge'; // Use Edge runtime
export async function GET(request) {
const { geo } = request;
// Access geo information from the Edge
const country = geo?.country || 'Unknown';
const city = geo?.city || 'Unknown';
const region = geo?.region || 'Unknown';
return NextResponse.json({
country,
city,
region,
message: `Hello from ${city}, ${region}, ${country}!`,
});
}// middleware.js
import { NextResponse } from 'next/server';
import { verifyToken } from './lib/auth';
// This middleware runs on the Edge
export async function middleware(request) {
// Only apply to API routes
if (request.nextUrl.pathname.startsWith('/api/')) {
// Check for authentication token
const token = request.headers.get('authorization')?.split(' ')[1];
if (!token) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
);
}
try {
// Verify the token
const decoded = await verifyToken(token);
// Add user info to headers for downstream use
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', decoded.userId);
// Continue with modified headers
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
} catch (error) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
}
// Continue for non-API routes
return NextResponse.next();
}
export const config = {
matcher: '/api/:path</em>',
};Developer Experience#
React.js: React provides flexibility but requires more decisions and setup. The ecosystem is vast but fragmented, requiring developers to choose and integrate various tools. Next.js: Next.js offers a more opinionated, batteries-included approach:• Integrated development environment
• Fast Refresh for instant feedback
• Built-in TypeScript support
• Comprehensive CLI tools
• Vercel integration for seamless deployment
• AI-assisted development with v0 (new in 2024-2025)
React.js Example: Project SetupSetting up a React project requires multiple decisions and configurations:
# Create a React project
npx create-react-app my-app
cd my-app
# Add routing
npm install react-router-dom
# Add state management
npm install redux react-redux @reduxjs/toolkit
# Add styling solution
npm install styled-components
# Add form handling
npm install react-hook-form zod
# Add testing libraries
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
# Configure ESLint and Prettier
npm install --save-dev eslint prettier eslint-config-prettier
# Create configuration files
touch .eslintrc.js .prettierrcNext.js provides a more streamlined setup experience:
# Create a Next.js project
npx create-next-app my-app
cd my-app
# Everything is included:
# - Routing
# - API routes
# - Image optimization
# - Font optimization
# - ESLint configuration
# - TypeScript support
# - Styling options (CSS Modules, Sass, Tailwind)
# - Testing setupNext.js also provides helpful CLI commands:
# Generate TypeScript types from your database schema
npx next-typegen
# Analyze your application's bundle size
npx next analyze
# Check for performance issues
npx next lint --report-performance
# Generate middleware and API route templates
npx next-create api users
npx next-create middleware authPerformance Features#
React.js: React's performance optimizations focus on the virtual DOM and rendering efficiency. Additional performance features require manual implementation. Next.js: Next.js includes advanced performance features:• Automatic Static Optimization
• Edge Runtime for global low-latency
• Streaming Server Rendering
• React Server Components integration
• Partial Prerendering (new in 2024)
• Advanced caching strategies
React.js Example: Performance OptimizationWith React, performance optimization is largely manual:
// React.js performance optimization
import { memo, useMemo, useCallback } from 'react';
// Memoize expensive component
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
console.log('Expensive component rendering');
// Complex rendering logic
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
});
function ProductList({ products, onProductSelect }) {
// Memoize expensive calculations
const sortedProducts = useMemo(() => {
console.log('Sorting products');
return [...products].sort((a, b) => a.name.localeCompare(b.name));
}, [products]);
// Memoize callback functions
const handleProductSelect = useCallback(
productId => {
console.log('Product selected:', productId);
onProductSelect(productId);
},
[onProductSelect]
);
return (
<div>
<h1>Products</h1>
<ExpensiveComponent data={sortedProducts} />
{sortedProducts.map(product => (
<button
key={product.id}
onClick={() => handleProductSelect(product.id)}
>
Select {product.name}
</button>
))}
</div>
);
}Next.js 14+ introduced Partial Prerendering, which combines static and dynamic content:
// app/dashboard/page.js
import { Suspense } from 'react';
import { DashboardHeader, StaticSidebar } from '@/components/dashboard';
import { LiveDataSection } from '@/components/dashboard/live-data';
// This component uses Partial Prerendering
export default function Dashboard() {
return (
<div className="dashboard-layout">
{/<em> These parts are static and prerendered at build time </em>/}
<DashboardHeader />
<StaticSidebar />
{/<em> This part is dynamic and rendered at request time </em>/}
<Suspense fallback={<p>Loading live data...</p>}>
<LiveDataSection />
</Suspense>
</div>
);
}
// Static components are prerendered
function DashboardHeader() {
return <header>Dashboard</header>;
}
function StaticSidebar() {
return (
<aside>
<nav>{/<em> Static navigation </em>/}</nav>
</aside>
);
}
// Dynamic component is rendered at request time
async function LiveDataSection() {
// This data is fetched at request time
const data = await fetch('https://api.example.com/live-data', {
cache: 'no-store', // Never cache this data
}).then(res => res.json());
return (
<section>
<h2>Live Data</h2>
<p>Last updated: {new Date().toLocaleTimeString()}</p>
<ul>
{data.map(item => (
<li key={item.id}>
{item.name}: {item.value}
</li>
))}
</ul>
</section>
);
}• Automatic metadata API
• Structured data helpers
• Open Graph and Twitter card support
• Robots.txt and sitemap.xml generation
• Web Vitals monitoring and optimization
React.js Example: SEO ImplementationWith React, SEO implementation requires additional libraries:
// React.js with react-helmet for SEO
import { Helmet } from 'react-helmet-async';
function ProductPage({ product }) {
// Format structured data for product
const structuredData = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.imageUrl,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'USD',
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
};
return (
<>
<Helmet>
<title>{product.name} | Our Store</title>
<meta name="description" content={product.description} />
{/<em> Open Graph tags </em>/}
<meta property="og:title" content={product.name} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.imageUrl} />
<meta
property="og:url"
content={`https://example.com/products/${product.id}`}
/>
<meta property="og:type" content="product" />
{/<em> Twitter Card tags </em>/}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={product.name} />
<meta name="twitter:description" content={product.description} />
<meta name="twitter:image" content={product.imageUrl} />
{/<em> Structured data </em>/}
<script type="application/ld+json">
{JSON.stringify(structuredData)}
</script>
</Helmet>
<div>
<h1>{product.name}</h1>
<img src={product.imageUrl} alt={product.name} />
<p>{product.description}</p>
<p>${product.price}</p>
</div>
</>
);
}Next.js provides a built-in metadata API:
// app/products/[id]/page.js
import { ProductDetails } from '@/components/product';
// Generate metadata for SEO
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
return {
title: `${product.name} | Our Store`,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [
{
url: product.imageUrl,
width: 1200,
height: 630,
alt: product.name,
},
],
type: 'product',
},
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.imageUrl],
},
// Structured data
other: {
'application/ld+json': JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.imageUrl,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'USD',
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
}),
},
};
}
// Automatically generate static paths for products
export async function generateStaticParams() {
const products = await getProducts();
return products.map(product => ({
id: product.id.toString(),
}));
}
// Product page component
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <ProductDetails product={product} />;
}Enterprise Features#
React.js: React provides the foundation for enterprise applications but requires additional tools and configurations for enterprise-grade features. Next.js: Next.js includes enterprise-ready features out of the box:• Role-based access control patterns
• Multi-tenant architecture support
• Internationalization (i18n) with automatic locale detection
• A/B testing infrastructure
• Analytics integration
• Enterprise-grade security features
React.js Example: Enterprise AuthenticationWith React, implementing enterprise authentication requires third-party libraries:
// React.js with Auth0 integration
import { useAuth0 } from '@auth0/auth0-react';
import { Navigate, Outlet } from 'react-router-dom';
// Authentication provider setup
function AppWithAuth() {
return (
<Auth0Provider
domain="your-domain.auth0.com"
clientId="your-client-id"
authorizationParams={{
redirect_uri: window.location.origin,
audience: 'https://api.example.com',
scope: 'read:users update:users',
}}
>
<App />
</Auth0Provider>
);
}
// Protected route component
function ProtectedRoute({ requiredPermissions = [] }) {
const { isAuthenticated, isLoading, user, getAccessTokenSilently } =
useAuth0();
const [hasPermissions, setHasPermissions] = useState(false);
useEffect(() => {
async function checkPermissions() {
if (!isAuthenticated || !requiredPermissions.length) {
setHasPermissions(isAuthenticated);
return;
}
try {
const token = await getAccessTokenSilently();
const response = await fetch('https://api.example.com/permissions', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const userPermissions = await response.json();
// Check if user has all required permissions
const hasAllPermissions = requiredPermissions.every(permission =>
userPermissions.includes(permission)
);
setHasPermissions(hasAllPermissions);
} catch (error) {
console.error('Error checking permissions:', error);
setHasPermissions(false);
}
}
checkPermissions();
}, [isAuthenticated, requiredPermissions, getAccessTokenSilently]);
if (isLoading) {
return <div>Loading...</div>;
}
return isAuthenticated && hasPermissions ? (
<Outlet />
) : (
<Navigate to="/login" />
);
}
// Usage in routes
function AppRoutes() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
{/<em> Protected admin routes </em>/}
<Route
element={<ProtectedRoute requiredPermissions={['admin:access']} />}
>
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/admin/users" element={<UserManagement />} />
</Route>
{/<em> Protected editor routes </em>/}
<Route
element={<ProtectedRoute requiredPermissions={['editor:access']} />}
>
<Route path="/editor" element={<EditorDashboard />} />
<Route path="/editor/content" element={<ContentManagement />} />
</Route>
</Routes>
);
}Next.js provides more integrated authentication patterns:
// middleware.js - RBAC with Next.js middleware
import { NextResponse } from 'next/server';
import { verifyAuth } from './lib/auth';
// Role-based access mapping
const ROLE_PERMISSIONS = {
admin: ['/admin', '/admin/users', '/admin/settings'],
editor: ['/editor', '/editor/content'],
user: ['/dashboard', '/profile'],
};
export async function middleware(request) {
const token = request.cookies.get('auth-token')?.value;
if (!token) {
const url = new URL('/login', request.url);
url.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
try {
// Verify token and extract user data
const { user } = await verifyAuth(token);
const { role } = user;
// Check if user has permission to access the requested path
const requestedPath = request.nextUrl.pathname;
const hasPermission = ROLE_PERMISSIONS[role]?.some(
path => requestedPath === path || requestedPath.startsWith(`${path}/`)
);
if (!hasPermission) {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
// Add user info to headers for downstream use
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', user.id);
requestHeaders.set('x-user-role', role);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
} catch (error) {
// Token is invalid or expired
const url = new URL('/login', request.url);
url.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
}
export const config = {
matcher: ['/admin/:path<em>', '/editor/:path</em>', '/dashboard', '/profile'],
};Next.js makes it easy to implement multi-tenant applications:
// middleware.js - Multi-tenant routing
import { NextResponse } from 'next/server';
export async function middleware(request) {
const { pathname, hostname } = request.nextUrl;
// Extract tenant from subdomain
const tenant = hostname.split('.')[0];
if (tenant !== 'www' && tenant !== 'app') {
// Add tenant information to headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-tenant', tenant);
// Rewrite to tenant-specific API routes
if (pathname.startsWith('/api/')) {
const url = request.nextUrl.clone();
url.pathname = `/api/tenants/${tenant}${pathname.substring(4)}`;
return NextResponse.rewrite(url, {
request: { headers: requestHeaders },
});
}
// Pass tenant info to all requests
return NextResponse.next({
request: { headers: requestHeaders },
});
}
return NextResponse.next();
}
export const config = {
matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
};Next.js provides built-in i18n support:
// next.config.js
module.exports = {
i18n: {
locales: ['en', 'fr', 'de', 'es', 'ja'],
defaultLocale: 'en',
localeDetection: true,
},
};
// app/[lang]/products/[id]/page.js
import { getDictionary } from '@/lib/dictionaries';
export async function generateMetadata({ params }) {
const { lang, id } = params;
const dictionary = await getDictionary(lang);
const product = await getProduct(id, lang);
return {
title: `${product.name} | ${dictionary.common.storeName}`,
description: product.description,
};
}
export async function generateStaticParams() {
const products = await getAllProducts();
const locales = ['en', 'fr', 'de', 'es', 'ja'];
return locales.flatMap(lang =>
products.map(product => ({
lang,
id: product.id.toString(),
}))
);
}
export default async function ProductPage({ params }) {
const { lang, id } = params;
const dictionary = await getDictionary(lang);
const product = await getProduct(id, lang);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<div className="price">
{dictionary.products.price}: {product.price}{' '}
{dictionary.common.currency}
</div>
<button>{dictionary.products.addToCart}</button>
</div>
);
}Conclusion: When to Choose React.js vs Next.js in 2025#
Choose React.js When:- You need maximum flexibility: React gives you complete control over your architecture and tooling choices.
- You're building a client-side only application: For applications like admin dashboards or internal tools where SEO isn't a concern.
- You're integrating with an existing backend: If you already have a robust API-driven backend, React can be a good frontend choice.
- You're building a mobile app with React Native: Using React for your web app maintains consistency with your mobile codebase.
- You need specialized optimizations: For applications with unique performance requirements that need custom build configurations.
- SEO is important: For content-driven websites, e-commerce, or any public-facing application where search engine visibility matters.
- You need server-side rendering or static generation: For improved performance and user experience.
- You want an all-in-one solution: When you prefer a framework that handles routing, data fetching, and API endpoints.
- You're building a large-scale application: Next.js provides structure and conventions that help maintain large codebases.
- Time-to-market is crucial: Next.js accelerates development with its built-in features and optimizations.
- You need enterprise features: For applications requiring internationalization, authentication, and multi-tenant support.
In 2025, many teams are taking a hybrid approach:
• Using Next.js for their public-facing websites and e-commerce platforms
• Using React for internal tools and dashboards
• Sharing components, hooks, and business logic between both
This approach leverages the strengths of both technologies while maintaining code reusability.
Final ThoughtsThe choice between React.js and Next.js isn't about which is "better" but rather which is more appropriate for your specific use case. React.js continues to excel as a flexible, lightweight library for building user interfaces, while Next.js has matured into a comprehensive framework that addresses many of the challenges of modern web development.
As web development continues to evolve, both React.js and Next.js are likely to remain dominant forces in the JavaScript ecosystem, each serving different but complementary needs in the developer community.


