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.
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:
- 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:
- create-react-app
- Gatsby
- 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.
- Updating package.json and dependencies
- Static Assets and Compiled Output
- 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"
},
Initial run scripts
"scripts": {
"build": "NODE_ENV=production webpack",
"start": "NODE_ENV=development webpack serve"
},
Based on the guide, since I had no react-scripts, I just had to install Next.js
npm i next
and also replace the initial run scripts to
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
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:
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)
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.
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.
- Moved my sample pdfs into
/public
- Moved my
/components
folder into this folder - Moved the entry point file
index.jsx
into this folder, and changed it to_app.js
- Moved the public html document
index.html
into this folder and changed it toindex.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 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:
-
*.css
β*.module.css
- 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.
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";
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 π
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.
End
This the final folder structure.
My site now looks and works the same as before migrating π
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.
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 !