At Graphite, we believe Next.js is the future. For anyone developing scalable, high-performance web applications, you need to migrate away from Create React App (CRA) and instead move to the Next.js framework.
Now, you may read this and think that sounds challenging. So did we, at first! Our codebase is structured in a sprawling, typescript monorepo, that dozens of developers collaborate on simultaneously. This made things a little more complicated, and our first attempts at migration were incredibly long, expensive, and failed partway through. However, after talking to members of the Vercel team, and using the new App Router, we condensed the entire migration into 5 simple PRs.
Before we tackle that though, let’s look at the difference between Next.js and CRA, and why we would even want to migrate in the first place.
There are two main differences between Next.js and single-page applications, such as those created by Create React App:
Next.js supports multiple rendering strategies including server-side rendering (SSR), static site generation (SSG), incremental static regeneration (ISR), and client-side rendering (CSR). Depending on the specific page's needs, developers can choose whether to render at compile time, on the server, or in the browser.
Because of this, the two also differ in how they handle routing. Next.js uses a file-system-based router built on App Router. By simply adding files to the 'pages' directory, routes are automatically available (and rendered by the selected strategy). Dynamic routing is also supported by creating files or folders with square bracket notation, e.g.,
Create React App on the other hand does not provide built-in routing. Instead, developers often integrate libraries like react-router to handle SPA-style navigation, where route changes do not trigger full page reloads but dynamically change page-contents based on the URL.
These two differences allow Next.js to optimize performance and improve developer experience in ways that just aren’t possible in a traditional single-page application model.
Along with bundle-splitting, Next.js’s out-of-the-box optimizations like automatic image optimization, and smart prefetching, contribute to faster load times and overall user experience. Since Next.js supports both server-side rendering(SSR) and static site generation(SSG), it can deliver fully-rendered pages to the client, which can significantly improve perceived performance, especially on slower networks.
The web development landscape is rapidly evolving and new tools and frameworks are constantly emerging. Next.js is the latest in that category, offering increased flexibility, scalability, and performance improvements without adding much overhead. Backed by Vercel, Next.js has plenty of development resources behind it while at the same time enjoying the benefits of a strong community and ecosystem. It also seamlessly integrates with the JAMstack architecture, which is becoming the go-to for modern web development due to its scalability and performance benefits. More and more developers are moving to Next.js.
Knowing the differences between the two, let’s take a look at the 5 pull requests that made our migration possible.
The first problem we had was an API difference between react-scripts (the library CRA uses to power
npm start) and Next.js. React-scripts has the flag
HTTPS=true to serve content using HTTPS in local development. Unfortunately Next.js doesn’t have anything analogous, expecting you to use a third-party tool for that.
Following a stack overflow post, we adopted local SSL proxy. This allowed us to serve local host through HTTPS, ensuring that our local development would remain unchanged.
Create React App has a lot of built-in magic to allow you to import SVGs in JSX. Honestly it magically supports the import of all sorts of files, but the main one we cared about was SVGs. This was achieved using a specific configuration of the SVGr library which takes SVGs and converts them into react components. Unfortunately, Next.js doesn’t have this same support out of the box.
As a solution, in this PR, we imported SVGr and updated to its latest, recommended syntax that supports export as both a name component and as a default component. This entailed refactoring a handful of library imports but overall was a pretty small change.
So this one is probably specific to us. As we mentioned above, Next.js has this awesome built-in routing functionality. Unfortunately, to support that functionality , Next interprets files in the “pages” directory as routes and assumes that they have specific exports.. While we did have a “pages” directory, they were not Next routes and did not have the required exports.
This PR renamed the directory “pages” to “router-pages”.
This next pull request (pun intended) is where the bulk of the migration took place; this is where the magic happens.
This pull request included a couple of different changes that I’m going to break out into a few steps.
First thing we had to do was introduce the next.config.js file to configure Next.
We didn’t do anything special with the configuration other than disabling static images. We did this because moving to Next’s image serving right now would require a pretty big amount of lift. We realized though we could split this out into an upstream PR later. So for now we turned this off.
The next thing we did was tweak our Webpack configuration; all we did here was update webpack to use the right SVGr version.
Furthermore, two new files, layout.tsx and page.tsx, were introduced. While layout.tsx replicated the layout previously defined in an index.html in CRA, page.tsx effectively took over what used to be in the app.js.
Then because we’re coming from a single-page application, in the next.js config, we inject a rewrite, which says “hey for any page loaded, let’s just put you right at the index.” This allowed us to maintain our client-side routing pattern for the time being.
So here we just updated out gitignore to ignore the .next folder, something we didn’t have to think about before
And finally we had to create a bunch of test mocks to handle file imports. You gotta have your tests
These changes enabled us to drop in Next.js; the package.json was updated to replace references of CRA (or in our case, Create React App rewired) with Next commands.
The final PR was to handle server-side rendering or SSR. SSR introduces complexities when accessing objects like window and document. We added conditional checks before accessing objects exclusive to the browser environment. This prevented errors during server-side rendering (where JS might try to access window and run into window is undefined).
Additionally, Next renders content on the server (for initial render) and on the client (for updates), and reconciles them through a process called hydration. If these two do’nt match, Next throws an error. Because React Router does not work on the server, we implemented an initial empty render for both server and client. This was a temporary solution to avoid hydration mismatches that we can remove after we migrate to Next’s routing.
Beyond these primary steps, we also updated our Content Security Policy (CSP). Next.js injects inline scripts, which are disallowed under our CSP. We resolved this by adding a build step to hash all inline scripts.
We hope our little expedition showed you that migration to Next doesn’t have to be scary, and is well worth it. As we continue our development journey, we plan on fully integrating all the rest of what Next.js has to offer.