Andrew Evans Husband, engineer, FOSS contributor, and developer at AWS. Follow me at rhythmandbinary.com and andrewevans.dev.

Next.js vs. React: The developer experience

12 min read 3401

Next.js vs. React: The Developer Experience

Editor’s note: This article was updated on 3 February 2022 to incorporate information about the developer experience in React 18.

When choosing any software library or framework, one normally considers the developer experience. When I say “developer experience” here, I mean to say what it’s like for developers to actually do the task. Developers generally favor libraries or frameworks that are fun and easy to use. This is a major reason why we have the leading libraries and frameworks today.

In the world of React, Next.js has become a popular framework for “hitting the ground running.” As a React fan myself, I picked up Next.js a few months ago and have really enjoyed working with it on my projects. I liked it so much, that I even rewrote my personal blog using Next.js (check out my post on it).

Next.js builds on top of React to provide a streamlined development experience. There is a small learning curve with Next.js, but even developers new to the world of frontend can get up and running relatively quickly. That being said, there is definitely a different experience when building a project with Next.js vs. React.

This post compares the developer experience of Next.js vs. React. I’ll walk through some background first and then dive into more specifics, discussing what it’s like to initially start a project, build pages, retrieve data, use the documentation, and perform advanced actions with both Next.js and React.

I’ll be referring to a sample project that you can find in my GitHub repo here. This project shows two different implementations of a fan site about the hit show The Mandalorian. The react folder in the project is the React version, of course, and the next.js folder contains the Next.js version. Both projects work and should only need the standard npm install to get up and going.

You can use the following to jump to a relevant section of the tutorial:

How is Next.js different from React?

Before we dive into the actual developer experience, it helps to have some background.

A react banner

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 the popular features of React. 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 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:

.
├── README.md
├── package.json
├── node_modules
├── pages
│   ├── _app.js
│   ├── api
│   └── index.js
├── public
│   ├── favicon.ico
│   └── vercel.svg
├── styles
│   ├── Home.module.css
│   └── globals.css
└── yarn.lock (package-lock.json)

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:

        <div className="header__links">
            <Link href="/">
                <a className="header__anchor">Home</a>
            </Link>
            <Link href="/about">
                <a className="header__anchor">About</a>
            </Link>
        </div>

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.

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:

export default function App() {
    return (
        <Router>
            <section>
                <Header />
                <Switch>
                    <Route path="/episodes">
                        <EpisodesPage />
                    </Route>
                    <Route path="/season2">
                        <Season2Page />
                    </Route>
                    <Route path="/quotes">
                        <QuotesPage />
                    </Route>
                    <Route path="/">
                        <HomePage />
                    </Route>
                </Switch>
            </section>
        </Router>
    );
}

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

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 = () => {
    return (
        <nav className="header">
            <span>
                <Link href="/">Home</Link>
            </span>
            <span>
                <Link href="/episodes">Episodes</Link>
            </span>
            <span>
                <Link href="/season2">Season 2</Link>
            </span>
            <span>
                <Link href="/quotes">Quotes</Link>
            </span>
        </nav>
    );
};
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
├── pages
│   ├── _app.js
│   ├── _document.js
│   ├── components
│   │   └── Header.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.

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) => state.Episodes.episodes);
    useEffect(() => {
        // 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 (
        <section className="episodes">
            <h1>Episodes</h1>
            {episodes !== null &&
                episodes.map((episodesItem) => (
                    <article key={episodesItem.key}>
                        <h2>
                            <a href={episodesItem.link}>{episodesItem.key}</a>
                        </h2>
                        <p>{episodesItem.value}</p>
                    </article>
                ))}
            <div className="episodes__source">
                <p>
                    original content copied from
                    <a href="https://www.vulture.com/tv/the-mandalorian/">
                        here
                    </a>
                </p>
            </div>
        </section>
    );

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 with React Hooks and API calls as well. The added advantage of pulling in data with Next.js is just 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 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 (
        <>
            <section className="episodes">
                <h1>Episodes</h1>
                {episodes !== null &&
                    episodes.map((episodesItem) => (
                        <article key={episodesItem.key}>
                            <h2>
                                <a href={episodesItem.link}>{episodesItem.key}</a>
                            </h2>
                            <p>{episodesItem.value}</p>
                        </article>
                    ))}
                <div className="episodes__source">
                    <p>
                        original content copied from
                        <a href="https://www.vulture.com/tv/the-mandalorian/">here</a>
                    </p>
                </div>
            </section>
        </>
    );
}
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. Redux is great because it scales a common method for working with your application’s state. The process of creating actions, reducers, selectors, and side effects scales well no matter what your application might be doing.

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 that 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 since 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) => {
    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 then 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.

Working with the documentation

With any software project, good documentation can really help you to 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 that 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, the team members of Next.js are themselves very accessible in the open-source world.

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

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

The developer experience in React 18

Currently, both Next.js and Create React App are using React version 17.0.2. But soon, React 18 will be released, which introduces some changes to the React developer experience.

One of the biggest changes will be the new React root API, which changes how the App component gets rendered to the DOM. In React 17 and prior, this has been done using the ReactDOM.render method, which rendered a JSX to a DOM target:

import * as ReactDOM from 'react-dom';
import App from 'App';

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

// Initial render.
ReactDOM.render(<App tab="home" />, container);

// During an update, React would access
// the root of the DOM element.
ReactDOM.render(<App tab="profile" />, container);

With the new root API, you’ll identify the DOM container as the root first, then render JSX to it.
This makes multiple calls to render less repetitive, along with the some changes to hydration to better facilitate modern use cases like partial hydration:

import * as ReactDOM from 'react-dom';
import App from 'App';

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

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

// Initial render: Render an element to the root.
root.render(<App tab="home" />);

// During an update, there's no need to pass the container again.
root.render(<App tab="profile" />);

React will also try to batch state changes, meaning it will group state changes into one update to reduce repetitive rendering. Prior to React 18, batching was reserved only for state changes from event handlers, but other situations such as asynchronous state changes weren’t batched (meaning they’d trigger a separate render). In React 18, all state updates will be batched.

Another of the big additions coming in React 18 is the startTransition API. Prior to React 18, all state changes were treated with equal priority, so larger state changes would result in the hanging of the UI which isn’t the best user experience.

With React 18, you can wrap slower state changes in a startTransition callback, which will allow React to make sure the UI doesn’t hang while that state change is processed:

import { startTransition } from 'react';


// Urgent: important state changes the UI must wait for work like normal
urgentState(input);

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: state changes that shouldn't hang the UI
  nonUrgentState(input);
}); 

So if the above is trigged on typing into an input form, the first state change will always complete before I can continue typing, but the “transition” state change will cancel on additional input instead of hanging the UI till it completes. You can read more on transitions here.

Finally, React 18 brings some architectural changes to how it handles Server Side Rendering of applications, which will bring speed improvements to metaframeworks 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.

Final thoughts on Next.js vs. React

The developer experience is what makes engineers love what they do. I work professionally as an engineer, but the professional environment doesn’t always lend itself to the best developer experience.

In a perfect world, you’d find yourself on a great team of engineers with a great product, a strong user community, and powerful tools. In the real world, you find some of that, but usually one (or more) of those is lacking.

React and Next.js both provide great developer experiences in their own way. React lets you build things the way you want and is supported by a strong community. Next.js makes your life easier through several tools and conventions available out of the box, and it is backed by a very active open source community as well.

When it comes to the tooling itself, both React and Next.js were easy to get started. Going beyond the simple “hello world” project, it was fairly easy to find help, whether it be in the documentation or community resources.

Both React and Next.js have specific things they do well. As I’ve mentioned, I rewrote my personal blog with Next.js because I was a fan. It is a great tool to use for static sites and connects easily with the CI/CD pipeline I have set up. Additionally, it was easy to navigate when I wanted to add components to my site for the different pages.

React is a great addition to any project, and it can also scale if given the opportunity. React is more versatile than Next.js only because it is a library; it is up to the engineer to determine its implementation.

At the end of the day, both React and Next.js provide solid developer experiences. I hope the comparisons and discussions I’ve included here give some insight to 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 Twitter at @AndrewEvans0102.

LogRocket: Full visibility into your production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?

Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.

No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.

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 React apps — .

Andrew Evans Husband, engineer, FOSS contributor, and developer at AWS. Follow me at rhythmandbinary.com and andrewevans.dev.

2 Replies to “Next.js vs. React: The developer experience”

  1. Thanks for this write-up!

    One thing to note about using Next.js is that when debugging, the error messages are significantly less helpful than in React. This, in my opinion, kills the developer experience and my advice is that you need a good solid reason to start with/use Next.js. We didn’t have one other than “it seems cool and people seem to like it”.

Leave a Reply