How to add Next.js to an existing React application
When I was about to create this website, I honestly didn't have a plan. It basically started out as a side project during my parental leave with our youngest son, and I started to write about different languages and frameworks just to keep my head in the game while I was having my time off from my software engineering job. I wasn't even planning on making it public to anyone to begin with, it sort of just happened along the way as it grew.
So what I am trying to say with this, is that I basically just created my project with the create-react-app command and got started right away with my project.
But as time went by, and I gained my first few vistors and even some e-mails from people that were reading my posts and were using my APIs, I decided to really put some effort into it and see if I can create something bigger than just a side project. And I always knew the hard truth, that in order to get traffic into your website, you need a solid SEO, amongst other things. And by using a client side rendering application, the struggle is real...
So I decided to roll up my sleeves, and get Next.js into my application to get server side rendering. And today, I will share how I approached it, and how I was able to get my website up and running in just 2-3 days of work without any prior experience with Next.js.
Learn the basics
When it comes to learning new frameworks, languages or techniques I personally don't spend much time reading the documentation before starting the practical work. I prefer to learn by doing and search for the solution if I stumble on a problem that I can't solve myself. So what I was doing before I installed Next.js in my project, was to create some small example projects just to get familiar with the framework and how to do the basic things. But I didn't spend to much time in this stage, since I do have a quite long experience with React and software engineering in general, so I was confident I could pull it off.
Install Next.js
When I felt that I was ready, and more important, that I thought that Next.js could provide the right benefits to my application - I installed it.
I simply followed the guide on their website and did the following:
npm install next@latest
And then I had everything I needed to continue the journey to SSR.
Setting up Next.js
Setting up the stuff needed to run Next.js is so simple. You just need to add a folder called app (there are other ways as well, like pages folder etc) and then add the files and subdirectories that will represent the routes of your application. So I started with a basic page.tsx and a layout.tsx in the root of the app folder with some minimum amount of code to get started.
Example of layout:
export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> )
Example of page
export default function Page() { return <h1>Hello, Next.js!</h1>; }
And then in the package.json you need to add following scripts as well:
{ "scripts": { "dev": "next dev", "build": "next build", "start": "next start" } }
And then run:
npm run dev
to get the server running and the journey will begin from here.
Structure the app router
My first task was to replicate my current routing and pages to work with Next.js. And it was quite straight forward I would say. Everything is handled in the app folder, and for each subdirectory you add, it will represent a path in the url. And I didn't have that many static urls in my applications, so it was quite easy to do the transition.
So to give an example, imagine you have three routes
/about /contact /blog
Then your app folder should contain three subfolders called about, contact and blog and each of them should have their own page.tsx file that will be rendered when the url is entered, and preferably a layout.tsx where you can add custom things to your page, such as title and descripton and common layouts etc.
Dynamic routes
And if you have dynamic routes, for example blog/:id, you should name the subfolder in the blog folder to e.g [id] and then add a page.tsx in that folder as well, which will be the entry point for that route. And the id param will be passed in as a prop. Example:
export default function BlogPage({ params }: IBlogPage) { console.log(params.id); // 1 return <div>my blog page</div>; }
So after doing this, I had a solid ground to stand on, and was ready for my next task.
Router
Since I was now using next routing, I removed the react-router-dom and instead added next/navigation to my imports.
So if we want to get params or do navigations, we can use next/navigation components instead. Example:
import { useParams, useRouter } from "next/navigation"; const params = useParams(); // gets the params of dynamic routes const navigate = useRouter(); navigate.back(); // Will navigate back. navigate.push("/home"); // Will navigate to home
And for links, we can use the Link component, which works very similar to react-router-dom.
import Link from "next/link"; <Link href="/about" className={styles.linkStyle}> About us </Link>;
Common layout
I was using React router with outlet to render my header, footer and sidebar in all my pages around the app, and then in between them, I rendered each page. I also had my whole application wrapped in a context provider to get shared states, such as the app theme etc. So my next challenge, was to replicate this behaviour using Next.js.
The solution was quite straight forward as well. I did a component, that did all the wrapping with the header, footer and sidebar as well as the context provider. The component in short looked something like this:
function Content({ children }: any) { const { theme } = useContext(MyThemeContext); return ( <div className={theme === "dark" ? darkTheme : lightTheme}> <Header /> <Sidebar /> {children} <Footer /> </div> ); } export default function Layout({ children }: any) { return ( <ThemeContextProvider> <Content>{children}</Content> </ThemeContextProvider> ); }
Note that I have two functions here. The reason I had to do it like that, is that otherwise the Content component wouldn't get access to the state value in the context, which I was using to set correct styles etc.
And then, in all my layout.tsx files that I had added in my app router folders, I pretty much did like this:
import Layout from "components/layout/Layout"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <> <Layout>{children}</Layout> </> ); }
And then my pages got the header, footer and sidebar and access to the context.
Migrate the pages
This was the majority of the work. Since almost all my pages where using stuff like useState and other client side features, I had to adapt them as good as I could to serve as a server page. The ones that was too big and complex, got a use client directive at the top of the page, but for the most of them I manage to move the logic to the client components instead. But to be honest, the most of the work was just copy and paste.
I began with my smaller pages, such as the about page and similar, and then moved over to the bigger pages when I had got a good feeling of how I should approach the migration in the best possible way.
Fetching
Some of my pages where fetching data from an API, such as retrieving articles - like the one you are reading right now. And before the migration to Next.js, I did the fetching in a useEffect when the page was mounted, and displayed a loading spinner until the data was fetched. But I now wanted to make the call from the server, and then provide the full HTML. I have written a guide on exactly this, read it here.
But in short, my pages that were doing the fetching looked something like this after the migration was done:
async function handler(postId: string) { const response = await fetch(`${API_URL}/${postId}`); const text = await response.text(); return text; } export default async function Page({ params }: any) { const blogText = await handler(params.postId); return <div>{blogText}</div>; }
And then my pages came back all rendered and ready to be consumed by the browser.
Components
The components were the easiest part of this migration. I basically just moved them from my src folder that I had previously, and added them into a new folder called components. And for the ones that were using useState and other client side feature, I simply added the use client directive at the top, so that my pages could still be server pages.
Meta data
The one thing I was struggling the most with, was the fact that my pages wasn't updating their titles and descriptions. I had previously done it with ReactHelmet, but now I wanted it to be done on the server side so that my pages could be indexed much easier by search engines. The solution I found, was that we could add some nice stuff in our layout.tsx pages.
Note that I have a guide on setting titles and description that will cover this more deeply.
Static meta data
So for the pages where I had static titles and description, I added following to the layout files:
import { Metadata } from "next"; export const metadata: Metadata = { title: "My title", description: "My description", };
and then Next.js will apply it accordingly.
Dynamic meta data
And for my pages where I wanted the title and description to be applied dependent on the content, we can add an async function instead. So in our layout file, we can do something like this:
export async function generateMetadata({ params }: any) { const blogPost = await getBlog(params.blogId); return { title: blogPost.title, description: blogPost.desc, }; }
And then Next.js will apply the meta data to our page.
Clean up
When my pages were up and running, the meta data was in place and my components were all working fine - it was time for cleaning up the old stuff.
So when the transition was made, I deleted the react-scripts, react-router-dom and react-helmet to name a few.
Build and deploy
And when I had tried out the application and everything was working as before, it was time to do some real production testing.
Docker
I am using docker to build my application. And I host it on a Cloud Run service at Google. So what I needed to update, was only my Dockerfile basically.
After some trial and error and searching online, my Dockerfile ended up looking like this:
FROM node:18-alpine AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json ./ RUN npm install --production FROM node:18-alpine AS builder WORKDIR /app COPY /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED 1 RUN npm run build FROM node:18-alpine AS runner WORKDIR /app ENV NODE_ENV production ENV NEXT_TELEMETRY_DISABLED 1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY /app/.next ./.next COPY /app/node_modules ./node_modules COPY /app/package.json ./package.json COPY /app/public ./public USER nextjs EXPOSE 3000 ENV PORT 3000 CMD ["npm", "start"]
And since my Cloud Run service is triggering on my Github commits, I committed and pushed it to my repo and it built it automatically and deployed it to my production server.
I have a guide on setting up Cloud Run with automatical triggers to Github.
So the build and deploy part was the smoothest I would say, since the only thing I needed to change was my Dockerfile.
Summary
In this article, I shared how I did my migration from a regular React application with client side rendering, to a Next.js setup with server side rendering.
I was fortunate enough, to begin this migration quite early on. I didn't have that many pages and the complexity is not super advanced. This task only took me a couple of days, and if I were more used to Next.js, it wouldn't take even that long - but if the application is older, bigger and has a lot more complex features, the migration will of course take much longer time. And then one should ask themselves if it is really worth it. The main goal I am trying to achieve, is to climb the latter on Google search pages, and see if SSR can help me with that, and two days of work was definitely worth to at least try it out. I did learn a lot at least, which is very important in this field of work!
I hope you enjoyed this article, and I also hope that it could guide you in some way to do the transition to SSR. If you have any questions or feedback, feel free to contact us and I will be happy to assist in any way that I can.
Thanks for reading, and have a great day!