Editor’s note: This article was last updated by Jude Miracle on 17 April 2023 to incorporate information about React’s latest version. For more articles that compare React and Next.js, check out “Comparing Create React App vs. Next.js performance differences.”
Developers consider the developer experience when selecting a software library or framework. Libraries or frameworks that are easy and fun to use are preferred, leading to the popularity of top frameworks. 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 post provides a comparison of the developer experience between Next.js and React. The comparison covers starting a project, building pages, pulling data, using documentation, and performing advanced actions with both frameworks. By the end of this post, developers will have a better understanding of the differences between Next.js and React and which framework is better suited for their needs.
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, 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.
Jump ahead in this article:
Before we dive into the actual developer experience, it helps to have some background.
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 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.
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:
. ├── 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
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.
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 ( <Router> <section> <Header /> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/episodes" element={<EpisodesPage />} /> <Route path="/season2" element={<Season2Page />} /> <Route path="/quotes" element={<QuotesPage />} /> </Routes> </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 ├── 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 and Next.js are two commonly used tools for routing in React apps. Although React Router is an independent library, Next.js includes routing functionality built-in, as well as additional features such as server-side rendering and automatic code splitting.
React Router is a commonly employed library for implementing client-side routing in React applications. Its API allows routes to be defined and navigation between them to be achieved in a declarative manner. React Router can be obtained via npm
and imported into an app using the import statement. Once installed, routes and components can be defined for each route to enable its use within the app.
Using React Router involves defining routes and components for each route, which allows for easy navigation between pages without reloading the entire page. React Router offers various options for customizing routing behavior, including dynamic and nested routes. However, React Router requires additional setup to work with server-side rendering and SEO optimization.
Next.js is a popular framework for building React applications. Next.js incorporates routing functionality natively, as well as other features like server-side rendering and automatic code splitting. Its routing is file-based, with each page being defined as a separate file in the pages directory. The file name becomes the route path, and the file’s default export becomes the component rendered for that route.
Next.js built-in functionality provides a seamless approach to routing that eliminates the need for additional setup. Each page is defined as a separate file in the pages directory, with the file name becoming the route path and the default export becoming the component that is rendered for that route. Next.js also allows for dynamic routing by defining a file with square brackets in the file name that matches the dynamic parameter in the URL.
Deciding whether to use React Router or Next.js built-in functionality for routing depends on the particular needs of the 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.
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 }; }
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 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) => { 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.
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, 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 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 a very positive developer experience.
Since the release of React 18, there have been some updates to the developer experience in React. Here are some of the changes:
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 ( <div> <h1>Hello, World!</h1> </div> ); } const container = document.getElementById('root'); // Create a root const root = ReactDOM.createRoot(container); // Render the App component to the root root.render(<App />);
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.
In React 18, a new feature called automatic batching has been 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.
React 18 has made numerous enhancements to the Suspense API, which is utilized for managing 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) => response.json()); return ( <div> <h1>My Data:</h1> <Suspense fallback={<div>Loading data...</div>}> <DataDisplay data={data} /> </Suspense> </div> ); } function DataDisplay({ data }) { return ( <div> {data.map((item) => ( <div key={item.id}>{item.name}</div> ))} </div> ); } 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 is a new experimental feature in React 18 that allows developers to write components that can be executed on the server, improving performance and reducing the amount of JavaScript that needs to be sent to the client:
// server component function ServerCounter(props) { return { data: { count: props.initialCount, }, markup: ( <div> <h1>Count: {props.initialCount}</h1> <button onClick={() => props.setCount((count) => count + 1)}> Increment </button> </div> ), }; } // client component function ClientCounter(props) { const [count, setCount] = useState(props.initialCount); return ( <div> <h1>Count: {count}</h1> <button onClick={() => setCount((count) => count + 1)}>Increment</button> </div> ); } // usage in a Next.js page function HomePage(props) { const { data, markup } = ServerCounter.getServerData({ initialCount: 0 }); return ( <> {markup} <ClientCounter initialCount={data.count} /> </> ); } export default HomePage;
From the code above, we define a ServerCounter
function that returns an object containing the data and markup for the component. The data includes an initial count value, and the markup includes a button that increments the count when clicked.
We also define a ClientCounter
component that receives the initial count value as a prop and uses it to render a count value and a button that increments the count when clicked. Finally, we use the ServerCounter
function to get the data and markup for the component on the server, and we pass the initial count value to the ClientCounter
component as a prop. This allows us to render the server-generated markup on the page and hydrate the client component with the same initial data.
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.
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:
When to use Next.js:
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 Twitter at @AndrewEvans0102.
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>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
3 Replies to "Next.js vs. React: The developer experience"
Hi, Many thanks for your article :).
“using a store is very difficult”. Maybe I’m missing something but using Redux with my NextJS project was not a pain… You even have a clear example in the NextJS repo : https://github.com/vercel/next.js/tree/canary/examples/with-redux.
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”.
Great post! Next.js simplifies development with built-in features like routing and SSR, enhancing the developer experience. React offers flexibility but requires more setup. Both have their strengths! Kellton to know more