Simple tutorial to migrate a non-CRA React project to Next.js

lyqht

Estee Tey

Posted on September 26, 2021

Simple tutorial to migrate a non-CRA React project to Next.js

In this article, I will be demonstrating how I migrated a simple React project that is not bootstrapped by Create React App (CRA) to Next.js.

Optional: 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.

Start

Initial Project Structure ๐Ÿ“‚

This is the original React repository.

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.

There are no routes, no environment variables, no search engine optimization yet to keep the guide and process simpler to understand for anyone that is new to migrating non-CRA React apps to Next.js ๐Ÿงก.

This app is made using the react-pdf webpack5 sample repository as a base and then adding my own components on top of it, as part of my previous experiments for react-pdf.

So at the moment, the project just looks like this:

notes-drawer-demo.gif

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

In order to migrate to Next.js, I went to their documentation to search for migration guides. As of September 2021, there are only migration guides for:

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

But not all hope is lost! ๐Ÿ’ช

I read through the create-react-app migration guide and it actually helped a lot for me to migrate my non-CRA React app. 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

Initial dependencies

"dependencies": {
    "@chakra-ui/react": "^1.6.7",
    "@emotion/react": "^11.4.1",
    "@emotion/styled": "^11.3.0",
    "@popperjs/core": "^2.10.1",
    "framer-motion": "^4.1.17",
    "react": "^17.0.0",
    "react-dom": "^17.0.0",
    "react-pdf": "latest",
    "react-popper": "^2.2.5"
  },
  "devDependencies": {
    "@babel/core": "^7.12.0",
    "@babel/preset-env": "^7.12.0",
    "@babel/preset-react": "^7.12.0",
    "babel-loader": "^8.0.0",
    "copy-webpack-plugin": "^9.0.0",
    "css-loader": "^6.0.0",
    "html-webpack-plugin": "^5.1.0",
    "style-loader": "^3.0.0",
    "webpack": "^5.20.0",
    "webpack-cli": "^4.7.0",
    "webpack-dev-server": "^4.0.0"
  },
Enter fullscreen mode Exit fullscreen mode

Initial run scripts

"scripts": {
    "build": "NODE_ENV=production webpack",
    "start": "NODE_ENV=development webpack serve"
  },
Enter fullscreen mode Exit fullscreen mode

Based on the guide, since I had no react-scripts, I just had to install Next.js

npm i next
Enter fullscreen mode Exit fullscreen mode

and also replace the initial run scripts to

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
Enter fullscreen mode Exit fullscreen mode

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

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:
Enter fullscreen mode Exit fullscreen mode

Next.js has a specific way of structuring the project, and currently my project doesn't follow that structure yet. Let's fix this step by step. Since the error says I don't have a pages directory, then I will just make a pages directory. When I run npm run dev again, it seems to look ok initially, 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)
Enter fullscreen mode Exit fullscreen mode

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")
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>)
}
Enter fullscreen mode Exit fullscreen mode

In Next.js, they require 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>)
}
Enter fullscreen mode Exit fullscreen mode

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.


Possible hiccup(s) you may face in migrating

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";
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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".
Enter fullscreen mode Exit fullscreen mode

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`;
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก 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.


End

This the final folder structure.

image.png

My site now looks and works the same as before migrating ๐Ÿ˜‡

migrate-demo.gif

The full changelog can once again be found here.

And we're done! ๐ŸŽ‰


Bonus: GitHub security vulnerability check

After pushing this production-ready commit to the main branch, Github also raised a security vulnerability alert to me, which might be due to the migration to Next.js. It's really cool that they even have a feature for the users to choose to apply an automated to fix the security vulnerability.

image.png


Thanks for reading the article!

What's next: I will be trying to integrate Notion API to actually save notes in this side project and write an article about it later on. Look out for it! โœจ

If you enjoyed reading it, react ๐Ÿงก, feedback ๐Ÿ’ฌ, follow me ๐Ÿง here and Twitter !

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
lyqht
Estee Tey

Posted on September 26, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About