VOOZH about

URL: https://blog.logrocket.com/next-js-vs-react-developer-experience/

⇱ Next.js vs. React: The developer experience - LogRocket Blog


2024-12-03
7118
#nextjs#react
Andrew Evans
28304
πŸ‘ Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

Editor’s note: This article was last updated by Jude Miracle on 3 December 2024 to cover how changes in Next.js 13 and React 18 affect developer changes, such as concurrent rendering, automatic batching, the App Router, and more.

πŸ‘ Next.js Vs. React: The Developer Experience

A lot is taken into consideration when selecting a library or a framework. Among the React community, Next.js has become a popular choice for developers who want to get started quickly. Next.js builds on top of React to provide a streamlined development experience, although there is a slight learning curve.

This article will compare both technologies. But before we dive into a detailed comparison of both technologies, let’s review the differences we will cover:

  • How is Next.js different from React?: Next.js is a full-stack framework with built-in server-side rendering and static generation, while React is a frontend library focused primarily on client-side rendering
  • What do React vs. Next.js projects look like?: Next.js projects come with an opinionated structure and tools to streamline development, whereas React projects offer more flexibility, allowing developers to choose their own project setup and dependencies
  • Rendering approaches Next.js vs React: React primarily handles client-side rendering, while Next.js offers built-in support for Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR)
  • Building pages: Next.js uses file-based routing for quick page setup, while React requires a separate routing library, like React Router, to build and manage pages
  • React Router vs. Next.js: React relies on React Router for routing functionality, but Next.js has a built-in routing system that automatically maps files to routes, simplifying page navigation
  • Pulling in data: React handles data fetching through hooks and external state management, while Next.js provides built-in data fetching methods (getStaticProps, getServerSideProps) with automatic server-side rendering
  • Performance optimization in Next.js 13 and React 18: Next.js automatically handles SSR, code splitting, image optimization, etc., while React requires manual configuration or additional libraries for these features
  • Comparing Next.js vs. React documentation: Next.js documentation is centralized and focused on guiding users through its opinionated setup, while React documentation covers broader use cases and relies more on community-supported resources for advanced topics
  • Developer experience improvement in React 18 and Next.js 13: React 18 introduces concurrent rendering, automatic batching, etc., for improved performance, while Next.js 13 leverages these React updates within its framework for optimized server and client rendering experiences

πŸš€ Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

How is Next.js different from React?

πŸ‘ React Logo

React was originally created by Facebook and has become one of the most popular libraries in the frontend world today. React is easily extendable and can include features like routing as well as state management patterns with libraries like Redux. React is minimal in its footprint but can be customized for almost any project. For more about React on a high level, check out the official React documentation:

πŸ‘ Next.js Logo

Next.js was created on top of React in an effort to build an easy-to-use development framework. It was developed by Vercel (formerly Zeit) and makes use of many of React’s popular features. Right out of the box, Next.js provides things like pre-rendering, routing, code splitting, and webpack support. For more on Next.js, check out the official Next.js documentation.

What do React vs. Next.js projects look like?

With React, you can get up and running by installing Node.js on your machine and running npx create-react-app my-app. This will create a basic project structure with the src/App.js file as the entry point for the application.

You’ll also have a public folder where you can store assets, and the initial scaffold includes a service worker and a method to pull in Jest for testing. The initial scaffold looks like this:

β”œβ”€β”€ README.md
β”œβ”€β”€ package.json
β”œβ”€β”€ node_modules
β”œβ”€β”€ public
β”‚ β”œβ”€β”€ favicon.ico
β”‚ β”œβ”€β”€ index.html
β”‚ β”œβ”€β”€ logo192.png
β”‚ β”œβ”€β”€ logo512.png
β”‚ β”œβ”€β”€ manifest.json
β”‚ └── robots.txt
β”œβ”€β”€ src
β”‚ β”œβ”€β”€ App.css
β”‚ β”œβ”€β”€ App.js
β”‚ β”œβ”€β”€ App.test.js
β”‚ β”œβ”€β”€ index.css
β”‚ β”œβ”€β”€ index.js
β”‚ β”œβ”€β”€ logo.svg
β”‚ β”œβ”€β”€ reportWebVitals.js
β”‚ └── setupTests.js
└── yarn.lock (or package-lock.json)

With Next.js, you can get started by running npx create-next-app. This will scaffold out a project that already has a pages folder for the pages or routes and a public directory that hosts your assets. The initial scaffold looks like this:

.
β”œβ”€β”€ pages/
β”‚ β”œβ”€β”€ api/ # API routes
β”‚ β”œβ”€β”€ _app.js # Custom App component
β”‚ β”œβ”€β”€ _document.js # Custom Document component
β”‚ β”œβ”€β”€ index.js # Home page
β”‚ └── ... # Other pages
β”œβ”€β”€ public/ # Static assets
β”œβ”€β”€ styles/ # Global styles
β”‚ β”œβ”€β”€ globals.css
β”‚ β”œβ”€β”€ theme.css
β”‚ └── ...
β”œβ”€β”€ components/ # Reusable components
β”œβ”€β”€ lib/ # Utility functions and server-side code
β”œβ”€β”€ test/ # Test files
β”œβ”€β”€ .babelrc # Babel configuration
β”œβ”€β”€ .eslintrc # ESLint configuration
β”œβ”€β”€ next.config.js # Next.js configuration
β”œβ”€β”€ package.json # Dependencies and scripts
└── README.md

N.B., the latest Next.js releases offer two different routers: the App Router and the Pages Router. The folder structure above is used if you choose to use the Pages Router.

The files in the pages directory correlate to the routes in your application. The public directory holds your static files or images you want to serve, and it can be directly accessed β€” no need to use require or other traditional React methods to import pictures into components.

Within the pages directory, you’ll see an index.js file, which is the entry point of your application. If you want to navigate to other pages, you can use the router with Link, as you see here:

<pre class="language-javascript">
 <div className="header__links">
 <Link href="/">
 <a className="header__anchor">Home</a>
 </Link>
 <Link href="/about">
 <a className="header__anchor">About</a>
 </Link>
 </div>
</pre>

With regards to the developer experience, the initial scaffolding process is pretty straightforward for both Next.js and React. React, however, does require you to add libraries like React Router for routing, whereas Next.js offers that functionality out of the box with the Link component.

Additionally, the overall structure of your application is already guided by Next.js by having the pages directory to hold your containers, etc.

Rendering approaches: React vs. Next.js

React primarily uses client-side rendering. This means it loads a simple HTML framework and uses JavaScript to fill in the content directly in the browser. This approach makes the user experience very interactive, but it can slow down the initial load time because the browser must download and run JavaScript before rendering the page.

To enable server-side rendering with React, developers often use libraries like ReactDOMServer or frameworks like Express. This setup is flexible but needs more configuration to support server-side rendering or static site generation.

Here’s a typical React implementation:

function ProductPage() {
 const [products, setProducts] = useState([]);
 const [isLoading, setIsLoading] = useState(true);
 const [error, setError] = useState(null);

 useEffect(() => {
 async function fetchProducts() {
 try {
 const response = await fetch('/api/products');
 const data = await response.json();
 setProducts(data);
 } catch (err) {
 setError(err.message);
 } finally {
 setIsLoading(false);
 }
 }
 
 fetchProducts();
 }, []);

 if (isLoading) return <LoadingSpinner />;
 if (error) return <ErrorMessage message={error} />;

 return (
 <div className="products-grid">
 {products.map(product => (
 <ProductCard key={product.id} {...product} />
 ))}
 </div>
 );
}

While this approach offers excellent interactivity after the initial load, it comes with specific trade-offs:

// Initial HTML sent to browser
<!DOCTYPE html>
<html>
 <head><title>Product Page</title></head>
 <body>
 <div id="root"></div>
 <script src="/bundle.js"></script>
 </body>
</html>

Next.js multiple rendering methods

Server-side rendering (SSR)

Server-side rendering (SSR) creates the HTML for a page every time a request is made. This means the content is always current but can lead to slightly slower response times. SSR is helpful when the content is dynamic and needs to show the latest information, like dashboards or user-specific content. It is also important for SEO when the content cannot be generated in advance:

// pages/product/[id].js
import React from 'react';

export async function getServerSideProps({ params }) {
 const res = await fetch(`https://api.example.com/products/${params.id}`);
 const product = await res.json();

 return { props: { product } };
}

export default function ProductPage({ product }) {
 return (
 <div>
 <h1>{product.name}</h1>
 <p>{product.description}</p>
 <p>Price: ${product.price}</p>
 </div>
 );
}

The server generates complete HTML, improving initial page load and SEO.

Client-side rendering (CSR)

Although less common in Next.js, CSR can still be implemented for highly interactive components or pages. In CSR, the browser receives minimal HTML content, and JavaScript loads the data to display the page. CSR allows for rich user interaction but can create challenges for SEO since search engines can’t see the content until it is rendered on the client side. This method is useful for dynamic and personalized content where user interaction is key, such as in web applications, even if SEO is not a priority.

Static site generation (SSG)

With static site generation (SSG), your website pages are created as HTML files during the build process. This means they load quickly for users. SSG works best for content that doesn’t change often, like blogs, marketing pages, or documentation:

// pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
 const post = await getPostBySlug(params.slug);
 
 return {
 props: { post },
 // Page will be rebuilt at most once every hour
 revalidate: 3600
 };
}

export async function getStaticPaths() {
 const posts = await getAllPosts();
 
 return {
 paths: posts.map(post => ({
 params: { slug: post.slug }
 })),
 // Show 404 for non-existent slugs
 fallback: false
 };
}

export default function BlogPost({ post }) {
 return (
 <article>
 <h1>{post.title}</h1>
 <div dangerouslySetInnerHTML={{ __html: post.content }} />
 </article>
 );
}

Incremental static regeneration (ISR)

Incremental static regeneration (ISR) allows you to update static web pages while they are running. It combines the advantages of static site generation (SSG) and server-side rendering (SSR). ISR automatically refreshes static content in the background after a set time or when an API triggers an update. This is helpful when you need the speed of static pages with regular updates, like for ecommerce product pages:

// pages/products/[id].tsx
export async function getStaticProps({ params }) {
 const product = await fetchProduct(params.id);
 
 return {
 props: {
 product,
 lastUpdated: new Date().toISOString(),
 },
 // Page regenerates after 60 seconds
 revalidate: 60
 };
}

function ProductPage({ product, lastUpdated }) {
 return (
 <div>
 <ProductDetails {...product} />
 <small>Last updated: {formatDate(lastUpdated)}</small>
 </div>
 );
}

Hybrid rendering with React 18 and Next.js 13

Modern applications often require a mix of rendering strategies. Both React 18 and Next.js 13 enable hybrid rendering within the same application.

You can combine SSR and CSR in a React application without using Next.js. However, this requires a manual setup. You will need to use frameworks like Express or Koa, along with ReactDOMServer and Hydration for the CSR part:

// server.js
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './App';
import fetch from 'node-fetch';

const app = express();

app.get('*', async (req, res) => {
 const initialData = await fetch('https://jsonplaceholder.typicode.com/posts/1')
 .then(response => response.json());

 const appHtml = ReactDOMServer.renderToString(<App initialData={initialData} />);

 res.send(`
 <!DOCTYPE html>
 <html lang="en">
 <head><title>SSR + CSR Example</title></head>
 <body>
 <div id="root">${appHtml}</div>
 <script>
 window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};
 </script>
 <script src="/bundle.js"></script>
 </body>
 </html>
 `);
});

app.listen(3000, () => console.log('Server is running on port 3000'));

// Client component
// App.js
import React, { useState, useEffect } from 'react';

export default function App({ initialData }) {
 const [data, setData] = useState(initialData);

 useEffect(() => {
 if (!initialData) {
 // Fetch additional data client-side
 fetch('https://jsonplaceholder.typicode.com/posts/1')
 .then(response => response.json())
 .then(data => setData(data));
 }
 }, [initialData]);

 return <div>{data ? data.title : 'Loading...'}</div>;
}

// Client entry point
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const initialData = window.__INITIAL_DATA__;

ReactDOM.hydrate(<App initialData={initialData} />, document.getElementById('root'));

Next.js 13 allows for detailed control over how components are rendered:

// pages/mixed.js in Next.js for SSR + CSR
import { useEffect, useState } from 'react';

export async function getServerSideProps() {
 const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
 const initialPost = await res.json();
 return { props: { initialPost } };
}

export default function Mixed({ initialPost }) {
 const [post, setPost] = useState(initialPost);

 useEffect(() => {
 // Fetch updated data on the client
 fetch('https://jsonplaceholder.typicode.com/posts/1')
 .then(response => response.json())
 .then(data => setPost(data));
 }, []);

 return <div>{post.title}</div>;
}

The choice of rendering strategy should be based on your application’s specific needs:

  • Use CSR for highly interactive applications with fewer SEO requirements
  • Choose SSR for dynamic content requiring fresh data and SEO
  • Implement SSG for static content that rarely changes
  • Leverage ISR for static content needing periodic updates
  • Consider hybrid rendering for complex applications with varying requirements across different pages

Next.js’s ability to mix these rendering strategies within a single application offers significant advantages over traditional React applications, which typically require additional setup and configuration to achieve similar functionality.

Next.js vs. React: Building pages

Now we can begin to discuss real examples of React vs. Next.js with the sample application I mentioned at the beginning. Again, you can find it in the repo.

Building pages with React requires you to create a component and then pull in React Router to orchestrate transitions in your site. If you look in the react folder in the sample application, you’ll see what you would likely expect from a traditional React application:

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

export default function App() {
 return (
 &lt;Router&gt;
 &lt;section&gt;
 &lt;Header /&gt;
 &lt;Routes&gt;
 &lt;Route path="/" element={&lt;HomePage /&gt;} /&gt;
 &lt;Route path="/episodes" element={&lt;EpisodesPage /&gt;} /&gt;
 &lt;Route path="/season2" element={&lt;Season2Page /&gt;} /&gt;
 &lt;Route path="/quotes" element={&lt;QuotesPage /&gt;} /&gt;
 &lt;/Routes&gt;
 &lt;/section&gt;
 &lt;/Router&gt;
 );
}

Here, the Header, EpisodesPage, Season2Page2, QuotesPage, and HomePage are all components that React Router is routing the URL path to render.


Over 200k developers use LogRocket to create better digital experiences

πŸ‘ Image
Learn more β†’

If you look at the Next.js folder of the project, you’ll notice that the project is much leaner because the routes are all built into the pages folder. The Header component uses Link to route to the different pages, as you see here:

import Link from "next/link";

const Header = () =&gt; {
 return (
 &lt;nav className="header"&gt;
 &lt;span&gt;
 &lt;Link href="/"&gt;Home&lt;/Link&gt;
 &lt;/span&gt;
 &lt;span&gt;
 &lt;Link href="/episodes"&gt;Episodes&lt;/Link&gt;
 &lt;/span&gt;
 &lt;span&gt;
 &lt;Link href="/season2"&gt;Season 2&lt;/Link&gt;
 &lt;/span&gt;
 &lt;span&gt;
 &lt;Link href="/quotes"&gt;Quotes&lt;/Link&gt;
 &lt;/span&gt;
 &lt;/nav&gt;
 );
};
export default Header;

A high-level view of the Next.js project shows how easy it is to follow as well:

.
β”œβ”€β”€ README.md
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
β”œβ”€β”€ components
β”‚ └── Header.js
β”œβ”€β”€ pages
β”‚ β”œβ”€β”€ _app.js
β”‚ β”œβ”€β”€ _document.js
β”‚ β”œβ”€β”€ episodes.js
β”‚ β”œβ”€β”€ index.js
β”‚ β”œβ”€β”€ quotes.js
β”‚ β”œβ”€β”€ season2.js
β”œβ”€β”€ styles
β”‚ β”œβ”€β”€ _contact.scss
β”‚ β”œβ”€β”€ _episodes.scss
β”‚ β”œβ”€β”€ _header.scss
β”‚ β”œβ”€β”€ _home.scss
β”‚ β”œβ”€β”€ _quotes.scss
β”‚ β”œβ”€β”€ _season2.scss
β”‚ └── styles.scss
β”œβ”€β”€ public
β”‚ β”œβ”€β”€ HomePage.jpg
β”‚ └── favicon.ico
└── yarn.lock

When you want to build out pages for the React project, you must build the component and then add it to the router. When you want to build pages for the Next.js project, you just add the page to the pages folder and the necessary Link to the Header component. This makes your life easier because you’re writing less code, and the project is easy to follow.

React Router vs. Next.js

Routing is an essential feature of React applications that lets users move between pages without reloading. React Router and Next.js are two tools for routing, and each has its own benefits and drawbacks.

React Router is a popular library for client-side routing in React applications, offering a declarative API for defining routes and navigating without reloading the page. In its v6 release, React Router introduced features like nested routes, relative routing, and a simplified API.

Key features include nested layouts, dynamic routes, and relative routing, which improve maintainability and readability. However, React Router requires additional setup for server-side rendering and SEO, as it’s designed primarily for client-side routing. Here is how React Router works:

import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
 {
 path: '/',
 element: (
 <Layout>
 <Outlet />
 </Layout>
 ),
 children: [
 {
 path: '',
 element: <HomePage />,
 loader: () => fetchHomeData(),
 },
 {
 path: 'products',
 children: [
 {
 path: '',
 element: <ProductList />,
 loader: () => fetchProducts(),
 },
 {
 path: ':id',
 element: <ProductDetail />,
 loader: ({ params }) => fetchProduct(params.id),
 },
 ],
 },
 ],
 },
]);

function App() {
 return <RouterProvider router={router} />;
}

Next.js offers a simple and native file-based routing system. Each page in a Next.js app is defined as a file within the pages directory, and the file name automatically becomes the route path. This means you don’t need an extra routing library.

Next.js allows for server-side rendering (SSR), dynamic routing, and API routes, which helps developers manage both the frontend and backend in one codebase. This makes Next.js a great choice for applications that need SSR, static site generation, and SEO optimizations:

// app/layout.tsx
export default function RootLayout({ children }) {
 return (
 <html>
 <body>
 <nav>
 <Link href="/">Home</Link>
 <Link href="/products">Products</Link>
 </nav>
 {children}
 </body>
 </html>
 );
}

// app/products/page.tsx
async function ProductsPage() {
 const products = await fetchProducts();
 return (
 <section>
 {products.map(product => (
 <ProductCard key={product.id} product={product} />
 ))}
 </section>
 );
}

// app/products/[id]/page.tsx
async function ProductPage({ params }) {
 const product = await fetchProduct(params.id);
 return <ProductDetail product={product} />;
}

Let’s look at how both React Router and Next.js handle common routing scenarios.

Here’s a dynamic routing example:

// React router
{
 path: 'products/:id',
 element: <ProductDetail />,
 loader: ({ params }) => fetchProduct(params.id),
}


// Next.js 13

// app/products/[id]/page.tsx
export default async function Page({ params }) {
 const product = await fetchProduct(params.id);
 return <ProductDetail product={product} />;
}

Here’s an error handling example:

// React router
{
 path: 'products/:id',
 element: <ProductDetail />,
 errorElement: <ProductErrorBoundary />,
}


// Next.js 13

// app/products/[id]/error.tsx
'use client'
export default function Error({ error, reset }) {
 return (
 <div>
 <h2>Something went wrong!</h2>
 <button onClick={reset}>Try again</button>
 </div>
 );
}

Which routing solution is better?

Deciding whether to use React Router or Next.js’ built-in functionality for routing depends on the particular needs of your application. React Router is more versatile and provides a wider range of options for complex routing behavior. Additionally, it has a larger community and more online resources to assist in its implementation. On the other hand, Next.js built-in functionality offers a simpler and more straightforward approach to routing that integrates well with server-side rendering and SEO optimization.

Next.js vs. React: Pulling in data

With any application, you’ll always have a need to retrieve data. Whether it’s a static site or a site that leverages multiple APIs, data is an important component.

If you look in the react folder in my sample project, you’ll see the EpisodesPage component uses a Redux action to retrieve the episodes data, as you see here:

 const dispatch = useDispatch();
 // first read in the values from the store through a selector here
 const episodes = useSelector((state) =&gt; state.Episodes.episodes);
 useEffect(() =&gt; {
 // if the value is empty, send a dispatch action to the store to load the episodes correctly
 if (episodes.length === 0) {
 dispatch(EpisodesActions.retrieveEpisodes());
 }
 });
 return (
 &lt;section className="episodes"&gt;
 &lt;h1&gt;Episodes&lt;/h1&gt;
 {episodes !== null &amp;&amp;
 episodes.map((episodesItem) =&gt; (
 &lt;article key={episodesItem.key}&gt;
 &lt;h2&gt;
 &lt;a href={episodesItem.link}&gt;{episodesItem.key}&lt;/a&gt;
 &lt;/h2&gt;
 &lt;p&gt;{episodesItem.value}&lt;/p&gt;
 &lt;/article&gt;
 ))}
 &lt;div className="episodes__source"&gt;
 &lt;p&gt;
 original content copied from
 &lt;a href="https://www.vulture.com/tv/the-mandalorian/"&gt;
 here
 &lt;/a&gt;
 &lt;/p&gt;
 &lt;/div&gt;
 &lt;/section&gt;
 );

The Redux action retrieves the values from a local file:

import episodes from '../../config/episodes';

// here we introduce a side effect
// best practice is to have these alongside actions rather than an "effects" folder
export function retrieveEpisodes() {
 return function (dispatch) {
 // first call get about to clear values
 dispatch(getEpisodes());
 // return a dispatch of set while pulling in the about information (this is considered a "side effect")
 return dispatch(setEpisodes(episodes));
 };
}

With Next.js, you can leverage its built-in data fetching APIs to format your data and pre-render your site. You can also do all of the things you would normally do with React Hooks and API calls. The added advantage of pulling in data with Next.js is that the resulting bundle is prerendered, which makes it easier for consumers of your site.

In my sample project, if you go to the nextjs folder and the episodes.js page, you’ll see that the information on The Mandalorian episodes is actually constructed by the call to getStaticProps, so the actual retrieval of the data only happens when the site is first built:

function EpisodesPage({ episodes }) {
 return (
 &lt;&gt;
 &lt;section className="episodes"&gt;
 &lt;h1&gt;Episodes&lt;/h1&gt;
 {episodes !== null &amp;&amp;
 episodes.map((episodesItem) =&gt; (
 &lt;article key={episodesItem.key}&gt;
 &lt;h2&gt;
 &lt;a href={episodesItem.link}&gt;{episodesItem.key}&lt;/a&gt;
 &lt;/h2&gt;
 &lt;p&gt;{episodesItem.value}&lt;/p&gt;
 &lt;/article&gt;
 ))}
 &lt;div className="episodes__source"&gt;
 &lt;p&gt;
 original content copied from
 &lt;a href="https://www.vulture.com/tv/the-mandalorian/"&gt;here&lt;/a&gt;
 &lt;/p&gt;
 &lt;/div&gt;
 &lt;/section&gt;
 &lt;/&gt;
 );
}
export default EpisodesPage;
export async function getStaticProps(context) {
 const episodes= [...];
 return {
 props: { episodes }, // will be passed to the page component as props
 };
}

More advanced actions

Beyond the basic functions we’ve covered here, you also eventually will need to do something more complex.

One of the more common patterns you see with React applications at scale is to use Redux Toolkit. Redux Toolkit is great because it scales a common method for working with your application’s state. RTK simplifies state management by offering built-in tools for creating actions, reducers, and managing side effects, which helps minimize the amount of extra code needed. It includes useful functions like createSlice and configureStore, allowing for quicker setup of actions and reducers. Additionally, it integrates smoothly with Redux Thunk and other middleware, making it easier to handle asynchronous actions.

With React, this is a matter of defining a store and then building flows throughout your application. One of the first things I did was see if I could do this in my project with Next.js.

After some googling (and a few failed attempts), I found that because of the way that Next.js pre- and re-renders each page, using a store is very difficult. There are a few folks who have made implementations of Redux with Next.js, but it’s not as straightforward as what you’d see with a vanilla React app.

Instead of using Redux, Next.js uses data-fetching APIs that enable pre-rendering. These are great because your site becomes a set of static pieces that can be easily read by web crawlers, thus improving your site’s SEO.

This is a huge win because JS bundles have typically been difficult for crawlers to understand. Additionally, you can be more crafty with some of these APIs and generated assets at build time like RSS feeds.

My personal blog site is written with Next.js. I actually built my own RSS feed by using the getStaticProps API that comes with Next.js:

export async function getStaticProps() {
 const allPosts = getAllPosts(["title", "date", "slug", "content", "snippet"]);

 allPosts.forEach(async (post) =&gt; {
 unified()
 .use(markdown)
 .use(html)
 .process(post.content, function (err, file) {
 if (err) throw err;
 post.content = file;
 });
 });

 const XMLPosts = getRssXml(allPosts);
 saveRSSXMLPosts(XMLPosts);

 return {
 props: { XMLPosts },
 };
}

The getAllPosts and getRssXml functions convert the Markdown into the RSS standard. This can then be deployed with my site, enabling an RSS feed.

When it comes to more advanced features like Redux or pre-rendering, both React and Next.js have tradeoffs. Patterns that work for React don’t necessarily work for Next.js, which isn’t a bad thing because Next.js has its own strengths.

Overall, in implementing more advanced actions, the developer experience with Next.js sometimes can be more guided than you’d normally see with a React project.

Performance optimization: Next.js 13 vs. React 18

With React 18 and Next.js 13, developers can use new features that improve performance for both the client and server. Each framework offers unique ways to boost application speed and user experience. The choice between them depends on the specific performance goals of a project. Let’s assess how they perform in typical situations, using Lighthouse metrics from a product listing app as key measures:

πŸ‘ Product Listing App

We’ll evaluate performance using the following metrics:

  1. First Contentful Paint (FCP) – Measures how quickly the first visible content is displayed
  2. Largest Contentful Paint (LCP) – Tracks the time taken for the largest visible content to appear
  3. Total Blocking Time (TBT) – Reflects delays caused by heavy JavaScript execution
  4. Speed Index – Indicates how quickly content is visually displayed during page load
  5. Cumulative Layout Shift (CLS) – Captures visual stability by measuring layout shifts

Here’s the Lighthouse score comparison for a product listing app that was implemented in both React and Next.js:

πŸ‘ Lighthouse Performance Score For Ecommerce App Made With React

Next.js lighthouse score:

πŸ‘ Lighthouse Performance Score For Ecommerce App Made With Next.js

Metric React 18 Next.js 13
First Contentful Paint 1.0s 0.2s
Largest Contentful Paint 4.8s 2.1s
Total Blocking Time 140ms 0ms
Speed Index 1.8s 1.0s
Cumulative Layout Shift 0.0 0.086

Improved interactivity with concurrent rendering

React 18 excels in applications requiring high interactivity, such as dashboards, product filtering, product searching, forms, or shopping carts. Its concurrent features ensure that complex client-side interactions remain smooth and responsive:

  • Concurrent rendering for smooth UI: React 18’s concurrent rendering enables background rendering without blocking the main thread. For instance, while loading large datasets, the UI remains interactive, reducing perceived load times. In our example, the product listing app remained responsive under load, but its Speed Index of 1.8s and LCP of 4.8s lagged behind Next.js
  • Automatic batching for efficient updates: React 18 combines multiple state updates into one event. This change reduces unnecessary re-renders and improves performance on the client side. This is particularly useful for interactive components like product searching, product review forms, etc.

Here’s the implementation:

Import React, {useTransistion, useCallback} from "react";

export function ProductFilters({
 categories,
 filterAction
}: ProductFiltersProps) {
 const [isPending, startTransition] = useTransition();

 const handleSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
 e.preventDefault();
 const formData = new FormData(e.currentTarget);
 startTransition(() => {
 filterAction(formData);
 });
 }, [filterAction]);

 return (
 <form onSubmit={handleSubmit}>
 <select
 name="category"
 onChange={(e) => {
 startTransition(() => {
 filterAction(new FormData(e.target.form!));
 });
 }}
 >
 {categories.map(category => (
 <option key={category} value={category}>{category}</option>
 ))}
 </select>
 <button disabled={isPending}>
 {isPending ? 'Filtering...' : 'Apply'}
 </button>
 </form>
 );
}

Leveraging server components and static rendering

Next.js 13 improves on React 18’s features for handling multiple tasks at once. It also adds its own improvements for SSR and creating static pages. These features make it a great choice for websites focused on content, SEO, and applications like ecommerce shops or blogs:

  • Server components for optimized data loading: Next.js 13 processes non-interactive data on the server, minimizing the JavaScript sent to the client. This results in a significantly faster FCP (0.2s) compared to React 18’s 1.0s
  • Static rendering with getStaticProps: For static pages, Next.js pre-renders content at build time, delivering near-instant load times and boosting SEO. On the product listing app, this approach brought the LCP down to 2.1s compared to React 18’s 4.8s
  • Image optimization: Next.js 13 includes built-in image optimization through the next/image component, offering a seamless way to handle image loading, resizing, and performance improvements. It supports features like lazy loading, automatic resizing, and modern formats like WebP, all out of the box. This approach also helps bring the LCP down to 2.1s compared to React 18’s 4.8s

Here is the implementation of server component data loading:

import { fetchProducts } from '../lib/api';

export default async function ProductsPage() {
 const products = await fetchProducts();

 return (
 <div className='p-10'>
 <h1 className="text-3xl font-bold mb-8">Products</h1>
 <Suspense fallback={<ProductGridSkeleton />}>
 <ProductGrid
 products={products}
 />
 </Suspense>
 </div>
 );
}

Here’s an implementation of the image optimization:

export function ProductImage({ 
 src, 
 alt,
 priority = false 
}: ProductImageProps) {
 return (
 <div className="relative w-full pt-[100%]">
 <Image
 src={src}
 alt={alt}
 fill
 sizes="(max-width: 640px) 100vw,
 (max-width: 1024px) 50vw,
 33vw"
 priority={priority}
 className="absolute inset-0 w-full h-full object-contain p-4"
 />
 </div>
 );
}

While React 18 provides the flexibility for manual optimization, Next.js 13’s built-in features delivered better performance metrics with less configuration. The choice between the two frameworks should consider both the performance requirements and the development team’s expertise.

Comparing Next.js vs. React documentation

With any software project, good documentation can help you easily use tools, understand what libraries to use, etc. There are fantastic documentation options available for both React and Next.js.

As I mentioned in the intro, Next.js has a β€œlearn-by-doing” set of documentation that walks you through how to do things like routing and building components. React also has a similar setup, with multiple tutorials that explain the basics.

With React, you can also rely upon a great community of developers who have created content in blog posts, YouTube videos, Stack Overflow, and even the React docs themselves. This has been built over years of development as the library has matured.

With Next.js, there is less in the way of formal tutorials and more in the way of GitHub issues and conversations. As I built my personal blog site, there were times when I had to do significant googling to resolve Next.js issues. However, Next.js team members themselves are very accessible in the open source world.

Tim Neutkens, one of the Next.js team members, responded to me directly on Twitter when I wrote a post on Next.js. He helped me work on an issue and was really great to work with. Having community members be so accessible is a great strength.

Within the React community, many key members are also just as accessible. In both React and Next.js, the active community provides a very positive developer experience.

The developer experience in React 18 and Next.js 13

Since the release of React 18, there have been some updates to the developer experience in React. Here are some of the changes:

New Root API

The new Root API is now the recommended way to render applications in React 18. It’s a new way of rendering applications that allow for better performance and flexibility. With the new API, you create a root, identify the DOM container as the root, and then render JSX to it:



import ReactDOM from 'react-dom';

function App() {
 return (
 &lt;div&gt;
 &lt;h1&gt;Hello, World!&lt;/h1&gt;
 &lt;/div&gt;
 );
}

const container = document.getElementById('root');

// Create a root
const root = ReactDOM.createRoot(container);

// Render the App component to the root
root.render(&lt;App /&gt;);

The New Root API allows for more flexibility in rendering components and can improve performance by reducing the amount of work done during rendering. It can also be used in conjunction with other new features in React 18, such as automatic batching and Suspense, to create even more performant applications.

Concurrent rendering

Concurrent rendering, introduced in React 18, allows React to prepare multiple UI updates simultaneously without blocking the main thread. This approach results in smoother transitions and faster responses to user actions, even in highly interactive applications. This simplifies performance optimization in complex interfaces as React effectively manages rendering priority.

Next.js 13 uses concurrent rendering to improve server-side rendering (SSR) and client-side rendering (CSR). By enabling partial updates, Next.js can prioritize critical content, ensuring users see and interact with essential elements faster. This leads to improved load times and an overall more engaging user experience:

// Using startTransition for non-urgent updates
const [isPending, startTransition] = useTransition();
const [filterTerm, setFilterTerm] = useState('');

const handleFilter = (term) => {
 startTransition(() => {
 setFilterTerm(term); // Non-urgent update
 });
};

Automatic batching

In React 18, a new feature called automatic batching was introduced, which aims to enhance performance by minimizing the number of updates that require rendering. Unlike previous versions of React, which only batched updates initiated by user events, such as clicks or keypresses, automatic batching batches all updates, including those caused by asynchronous code or other sources.

The primary purpose of automatic batching is to consolidate several updates into a single batch, resulting in a substantial reduction in the number of updates that need to be rendered. This can improve performance and decrease the rendering workload.

In Next.js 13, automatic batching integrates well with Next.js data fetching and rendering techniques. This feature is useful in large-scale applications with multiple concurrent interactions, as it minimizes re-renders across server and client environments:

// Before React 18
setTimeout(() => {
 setCount(c => c + 1); // Causes a render
 setFlag(f => !f); // Causes a render
}, 1000);

// After React 18
setTimeout(() => {
 setCount(c => c + 1); // Batched
 setFlag(f => !f); // Batched
}, 1000); // Results in only one render

Suspense

React 18 and Next.js 13 made numerous enhancements to the Suspense API, which is used to manage asynchronous rendering and data retrieval in React applications. Suspense can be used with server-side rendering to allow your application to load and display data more efficiently. By suspending rendering until data is available, your application can provide a better user experience and improve performance:

import { Suspense } from 'react';

function MyComponent() {
 const data = fetch('/api/data').then((response) =&gt; response.json());

 return (
 &lt;div&gt;
 &lt;h1&gt;My Data:&lt;/h1&gt;
 &lt;Suspense fallback={&lt;div&gt;Loading data...&lt;/div&gt;}&gt;
 &lt;DataDisplay data={data} /&gt;
 &lt;/Suspense&gt;
 &lt;/div&gt;
 );
}

function DataDisplay({ data }) {
 return (
 &lt;div&gt;
 {data.map((item) =&gt; (
 &lt;div key={item.id}&gt;{item.name}&lt;/div&gt;
 ))}
 &lt;/div&gt;
 );
}

export default MyComponent;

In the example, when MyComponent is first rendered, the fetch request will be initiated and the fallback UI will be displayed until the data is available. Once the data is available, the DataDisplay component will be rendered with the fetched data.

Server components

React Server Components (RSC) represents a fundamental shift in React application architecture. This powerful feature combines the best of server-side rendering with client-side interactivity, creating a seamless development experience while significantly improving application performance.

Server Components shine in their ability to render on the server, sending only the necessary HTML to the client. This approach eliminates unnecessary JavaScript from the client bundle and enables direct backend access without API layers. The result is faster initial page loads, improved SEO, and enhanced data security since sensitive information like API keys can remain server-side.

React 18 introduced a new way to build server components. Here’s how to create a component that shows article details:

// Message.server.jsx
import { db } from './db.server';

async function ArticleDetails({ id }) {
 // Direct database access without API calls
 const article = await db.articles.get(id);
 
 return (
 <article className="article">
 <h1 className="article-title">{article.title}</h1>
 <div className="article-content">
 <p>{article.body}</p>
 </div>
 {/* Server components can contain client components */}
 <LikeButton articleId={id} />
 </article>
 );
}

export default ArticleDetails;

Next.js 13 and later versions improve on React’s server components. They offer a clear and straightforward way to use these components:

// app/page.tsx
import { LikeButton } from './components/LikeButton';
import { fetchArticles } from './lib/data';

export default async function Page() {
 const articles = await fetchArticles();
 
 return (
 <main className="articles-container">
 {articles.map(article => (
 <article key={article.id} className="article">
 <h2>{article.title}</h2>
 <p>{article.excerpt}</p>
 <LikeButton articleId={article.id} />
 </article>
 ))}
 </main>
 );
}

The main difference between React 18 and Next.js 13+ is how they handle server components. React 18 uses the .server.jsx file extension to identify server components. In contrast, Next.js 13+ treats all components in the app directory as server components by default. To specify client components in Next.js, you need to add the `use client` directive. This creates a clear separation between server and client code.

Data fetching in server components becomes straightforward and efficient. Here’s how you might implement it:

// lib/data.ts
export async function fetchArticles() {
 const res = await fetch('https://api.example.com/articles', {
 next: { revalidate: 3600 } // Cache for 1 hour
 });
 
 if (!res.ok) throw new Error('Failed to fetch articles');
 return res.json();
}

App Router

Next.js 13 introduced a new feature, App Router, which represents a complete rethinking of application routing and layout management. Its features include nested layouts and routes, a server-first approach, simplified data fetching, improved error boundaries, and built-in loading states:

app/
β”œβ”€β”€ layout.js
β”œβ”€β”€ page.js
β”œβ”€β”€ blog/
β”‚ β”œβ”€β”€ layout.js
β”‚ β”œβ”€β”€ page.js
β”‚ └── [slug]/
β”‚ └── page.js
└── shop/
 β”œβ”€β”€ layout.js
 └── page.js

Improved error handling

React 18 has implemented better error handling, which simplifies the process of identifying and addressing issues in your React apps. In the past, when a component encountered an error, React would cease rendering and present an error message in the browser console. Yet, in React 18, error boundaries have been enhanced to offer improved error handling and diagnostic details.

Finally, React 18 brings some architectural changes to how it handles server-side rendering of applications, which will bring speed improvements to meta frameworks like Next.js, Remix, and Gatsby. Instead of the entire application having to complete server-side rendering before it can hydrate in the user’s browser, using streaming HTML, the completed parts can be partially hydrated, giving the user a load time that feels faster. You can read more about these updates to SSR here.

The bottom line is that React 18 will bring many improvements to the developer experience for React and Next.js.

Notable applications built with React and Next.js

Companies choose between React and Next.js based on their needs. Let’s look at top applications built with these technologies

  • Facebook is the biggest user of React, the framework it created. The platform handles billions of interactions every day, showing React’s ability to manage complex data at a large scale. The news feed on Facebook illustrates how well React works with changing content
  • Instagram’s web app also uses React to handle a lot of images. Its image-heavy design benefits from React’s virtual DOM, allowing smooth transitions and quick updates
  • TikTok’s web platform uses Next.js for video content and benefits from server-side rendering. When they switched from regular React to Next.js, they saw significant performance gains
  • Notion changed how people collaborate on documents with Next.js. Their platform can render complex documents quickly while keeping load times fast and being effective for search engines
  • Spotify’s web player shows React’s ability to manage real-time audio streaming and complex data

TL;DR: Comparing React and Next.js

For a quick overview of these technologies, check out the comparison table below:

Feature React Next.js
Type JavaScript library Full-stack framework
Rendering Client-side only Client-side, server-side, static
Routing Requires React Router Built-in file-based routing
SEO Limited (CSR only) Better (SSR support)
Setup Manual configuration needed Zero configuration
Build size Lighter Heavier (more features)
Learning curve Moderate Steeper (more concepts)
Use case Single-page applications Production-ready applications

Final thoughts on Next.js vs. React

React and Next.js are both great choices for building web applications, but they each have their own strengths and weaknesses. By understanding what these are, you can choose the right tool for your project and create a better developer experience for yourself and your team.

When to use React:

  • React is a versatile library that allows you to build things the way you want. It gives developers much control over how they implement their projects, making it a great choice when you need to build custom components or functionality
  • React can also scale to larger projects, making it a good choice when you need to build more complex applications that require a lot of flexibility and customization

When to use Next.js:

  • Next.js is a framework that makes your life easier by providing many tools and conventions out of the box. It comes with features like server-side rendering, automatic code splitting, and more, which can help you get up and running quickly
  • Next.js is particularly well-suited for static sites, and it integrates easily with CI/CD pipelines. This makes it a good choice for projects where you need to build and deploy content quickly and easily

At the end of the day, React and Next.js provide solid developer experiences. I hope the comparisons and discussions I’ve included here give insight into how you could use them with your projects. I encourage you to check them out and check out my sample projects as well.

Thanks for reading my post! Follow me on andrewevans.dev and connect with me on X at @AndrewEvans0102.

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side

    $ npm i --save logrocket 
    
    // Code:
    
    import LogRocket from 'logrocket'; 
    LogRocket.init('app/id');
     
    // Add to your HTML:
    
    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
     
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
πŸ‘ Image
πŸ‘ Image
πŸ‘ Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

Debug Next.js apps with AI agents and next-browser

Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.

πŸ‘ Image
Emmanuel John
Jun 17, 2026 β‹… 9 min read

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

πŸ‘ Image
Chizaram Ken
Jun 16, 2026 β‹… 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension β€” no new framework required.

πŸ‘ Image
Ikeh Akinyemi
Jun 12, 2026 β‹… 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo β€” with email/password login, Google OAuth, session persistence, and protected routes.

πŸ‘ Image
Chinwike Maduabuchi
Jun 9, 2026 β‹… 13 min read
View all posts

Hey there, want to help make our blog better?

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 now