Migrate a non-CRA React project to Next.js

ยท

8 min read

Migrate a non-CRA React project to Next.js

In this tutorial, I will be demonstrating step by step how I migrated a React(JS) Single Page Application (SPA) project to Next.js. No knowledge of Next.js concepts is required to follow along. The article includes the issues that I've met and the resolutions I've made to fix them, so that you have a better idea when you try to migrate your own React project as well. To simulate most production apps that are ejected, the project for this tutorial is not bootstrapped by Create React App (CRA).

If you're keen to look at full changelog at a glance before we start, you can refer to this commit in the github repository.

Initial Project Structure ๐Ÿ“‚

This is the original repository. This app is created using the react-pdf webpack5 sample repository as a base and then adding custom components on top of it. I did this as part of my previous experiments for react-pdf.

image.png

If you prefer navigating files in your own editor, you can clone the repository and check out the before-migrate-to-nextjs branch.

So at the moment, the app has the following features:

  • you can upload a pdf and you can highlight text to get it in the input field.
  • when you click save note, it will save it in memory and show it below the input field.

notes-drawer-demo.gif


As of publishing the article on September 2021, on the Next.js documentation, there are only migration guides for:

  1. create-react-app
  2. Gatsby
  3. react-router

What about non-CRA React apps? ๐Ÿค”

Well, you can refer to this article! โœจ

It uses some of the steps listed in create-react-app migration guide.

Below are the sections that were relevant for this simple non-CRA React project.

  1. Updating package.json and dependencies
  2. Static Assets and Compiled Output
  3. Styling

Let's go through these steps!


1. Updating package.json and dependencies

Firstly, install Next.js.

npm i next

Then, we have to replace our existing run scripts that are based on webpack dev server.

Initial run scripts

"scripts": {
    "build": "NODE_ENV=production webpack",
    "start": "NODE_ENV=development webpack serve"
  },

Replaced run scripts

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }

To test it locally, I ran npm run dev, however, I couldn't start it.

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
info  - Using external babel configuration from /home/lyqht/Github/dr-teck/.babelrc
Error: > Couldn't find a `pages` directory. Please create one under the project root
    at Object.findPagesDir (/home/lyqht/Github/dr-teck/node_modules/next/dist/lib/find-pages-dir.js:33:11)
    at new DevServer (/home/lyqht/Github/dr-teck/node_modules/next/dist/server/dev/next-dev-server.js:101:44)
    at NextServer.createServer (/home/lyqht/Github/dr-teck/node_modules/next/dist/server/next.js:104:20)
    at /home/lyqht/Github/dr-teck/node_modules/next/dist/server/next.js:119:42
    at async NextServer.prepare (/home/lyqht/Github/dr-teck/node_modules/next/dist/server/next.js:94:24)
    at async /home/lyqht/Github/dr-teck/node_modules/next/dist/cli/next-dev.js:121:

This is because currently the project doesn't follow Next.js's required project structure yet. Let's fix this step by step. Since the error says I don't have a pages directory, then I will make a pages directory.

When I run npm run dev again, it seems to look ok , but when I try visiting localhost:3000, then there's yet another error ๐Ÿคฆโ€โ™‚๏ธ

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
info  - Using external babel configuration from /home/lyqht/Github/dr-teck/.babelrc
event - compiled successfully # this line made it look like it was gonna work
event - build page: /next/dist/pages/_error # this happens when I visit the site
wait  - compiling...
event - compiled successfully
ReferenceError: regeneratorRuntime is not defined
    at /home/lyqht/Github/dr-teck/.next/server/pages/_document.js:687:62
    at /home/lyqht/Github/dr-teck/.next/server/pages/_document.js:729:6
    at Object../node_modules/next/dist/pages/_document.js (/home/lyqht/Github/dr-teck/.next/server/pages/_document.js:733:2)
    at __webpack_require__ (/home/lyqht/Github/dr-teck/.next/server/webpack-runtime.js:25:42)
    at __webpack_exec__ (/home/lyqht/Github/dr-teck/.next/server/pages/_document.js:1365:39)
    at /home/lyqht/Github/dr-teck/.next/server/pages/_document.js:1366:28
    at Object.<anonymous> (/home/lyqht/Github/dr-teck/.next/server/pages/_document.js:1369:3)us> (/home/lyqht/Github/dr-teck/.next/server/pages/_document.js:1369:3)

Well, in this error, even though it is not explicit what is causing the regeneratorRuntime to be undefined, you can see the file where the exception is called in .next/server/pages/_document.js , a file within our project repository itself. This new .next build folder is generated by Next.js even when we start the app locally.

image.png

Upon reading further in the guide, I noticed that the _app.js and _document.js in the .next build folder are actually mentioned in the next section for Static Assets and Compiled Output.


2. Static Assets and Compiled Output

According to this section, in the /pages folder, there are specific files that Next.js looks for when they try to start the app. So these are the steps that I took.

  1. Moved my sample pdfs into /public
  2. Moved my /components folder into this folder
  3. Moved the entry point file index.jsx into this folder, and changed it to _app.js
  4. Moved the public html document index.html into this folder and changed it to index.js

Initial index.jsx

import { ChakraProvider } from "@chakra-ui/react";
import React from "react";
import { render } from "react-dom";
import PDFViewer from "./components/PDFViewer";

render(
  <ChakraProvider>
    <PDFViewer />
  </ChakraProvider>,
  document.getElementById("react-root")
);

After changing to _app.js

import { ChakraProvider } from "@chakra-ui/react";
import React from "react";
import PDFViewer from "./components/PDFViewer";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";

function MyApp({ Component, pageProps }) {
  return (
    <ChakraProvider>
      <PDFViewer />
    </ChakraProvider>
  )
}

export default MyApp

Initial index.html

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dr.Teck</title>
  </head>
  <body>
    <div id="react-root"></div>
  </body>
</html>

After changing to index.js

import Head from "next/head";
import Image from "next/image";

export default function Home() {
  return (
    <div>
      <Head>
        <meta name="description" content="Generated by create next app" />
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Dr.Teck</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <body>
        <div id="react-root"></div>
      </body>
    </div>
  );
}

And with all the changes above, we retry starting up the app locally with npm run dev. This time, we still have an error, but it looks a lot more simpler to fix! โœจ We are getting somewhere!

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
info  - Using external babel configuration from /home/lyqht/Github/dr-teck/.babelrc
(node:4318) [DEP_WEBPACK_MODULE_ISSUER] DeprecationWarning: Module.issuer: Use new ModuleGraph API
error - ./pages/components/Navbar.css
Global CSS cannot be imported from files other than your Custom <App>. Due to the Global nature of stylesheets, and to avoid conflicts, Please move all first-party global CSS imports to pages/_app.js. Or convert the import to Component-Level CSS (CSS Modules).
Read more: https://nextjs.org/docs/messages/css-global
Location: pages/components/Navbar.jsx

This error brings us to the next section on Styling.


3. Styling

In my initial components, say Navbar.jsx, if there is a custom stylesheet for that component, I would name it Navbar.css and import it as such

import "./Navbar.css";

const NavBar = ({someProp}) => {
     return (<div className={"sticky"}> ... </div>)
}

In Next.js, they need such component-specific stylesheets to be CSS Modules. Luckily, it is quite easy to convert the files into modules! We just have to do the following:

  1. *.css โ†’ *.module.css
  2. Change the way that we import the styles

After changing the way we import the style, the above example becomes

import styles from "./Navbar.module.css";

const NavBar = ({someProp}) => {
     return (<div className={styles.sticky}> ... </div>)
}

If I wasn't using react-pdf in my project, that would have been the end of the process of migrating from React to Next.js ๐Ÿคฏ.

Unfortunately there are certain packages that work a little differently, and the usual way of using them is not supported in the context of Next.js ๐Ÿ˜ข. This will be elaborated in the next section.


A webpack issue that you hopefully won't meet

This was how I have been using and importing components from react-pdf before migrating to Next.js - it is a service worker kind of implementation as recommended by the README.md of the react-pdf repository.

import { Document, Page } from "react-pdf/dist/esm/entry.webpack";

Now, I'm encountering the following error.

/home/lyqht/Github/dr-teck/node_modules/react-pdf/dist/esm/entry.webpack.js:1
import * as pdfjs from 'pdfjs-dist'; // eslint-disable-next-line
^^^^^^

SyntaxError: Cannot use import statement outside a module

So I thought hmm, okay maybe let's try going without the service worker implementation so I'm gonna just import it like normal libraries.

import { Document, Page } from "react-pdf";

Now, there's no more errors, and I'm finally able to visit my site! But happiness is short-lived, and my initial PDF was not able to load. There's also an error in the Console ๐Ÿ˜•

image.png

Error: Setting up fake worker failed: "Cannot load script at: http://localhost:3000/pdf.worker.js".

According to this Github issue, React PDF 4.x does not work without a service worker ๐Ÿ˜…. Thankfully, the Open source community is awesome and there were commenters that gave a working fix which involve setting the pdf.js service worker to a CDN version directly.

import { Document, Page, pdfjs } from "react-pdf";
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;

๐Ÿ’ก So, a takeaway here is that sometimes if you're trying to migrate a project to a new framework, be careful of such possible issues, and try to look them on the original Github repository for those offending packages.


4. Successfully starting the app locally

This the updated folder structure.

image.png

Now, my site is able to run locally and works the same as before migrating ๐Ÿ˜‡

migrate-demo.gif

The full changelog can once again be found here.


5. Deploying to Vercel

Vercel is a common platform to use for deploying Next.js applications since they are the ones who made the framework after all. All I needed to do is to just import the git repository and they will detect that the project is based on the Next.js framework.

However, if we are deploying it as it is now, then we would meet a problem.

image.png

This happens because not every file in the pages folder has a default export. Well, the simple fix for that is to just move out components folder out to the root folder and update imports accordingly. Having only page components in the pages folder aka file based routing is Next.js's pattern of routing in place of standard react-router switch patterns.


That's a wrap folks! ๐ŸŽ‰

birds excited

Thank you for reading, hope you enjoyed the article!

If you find the article awesome, hit the reactions ๐Ÿงก and share it ๐Ÿฆ~

To stay updated whenever I post new stuff, follow me on Twitter.

Did you find this article valuable?

Support Estee Tey by becoming a sponsor. Any amount is appreciated!

ย