DEV Community

Cover image for Migrating My Portfolio from CRA to Next.js App Router: Lessons Learned
Thomas T
Thomas T

Posted on

Migrating My Portfolio from CRA to Next.js App Router: Lessons Learned

As part of improving my personal portfolio site, I decided to migrate it from Create React App (CRA) to the latest Next.js App Router. While the overall benefits of the App Router are clear (including better SEO, file-based layouts, and built-in server-side rendering), the migration process introduced several non-trivial challenges.

This article documents the specific hurdles I faced and how I resolved them.


1. Hydration Mismatch from Chakra UI

After integrating Chakra UI with the App Router, I encountered this warning:

Warning: Text content did not match. Server: “undefined” Client: “light”

This occurred because Chakra UI dynamically injects a data-theme attribute on the client, which does not exist on the server-rendered HTML. This difference triggered a hydration mismatch warning from React.

Resolution

I added the suppressHydrationWarning attribute to the <html> tag to suppress this warning in non-critical areas:

<Html lang="en" suppressHydrationWarning>
Enter fullscreen mode Exit fullscreen mode

2. Layout Flicker Between Mobile and Desktop Header

Upon loading, the header would briefly display the mobile layout (even on desktop) before correcting itself. This issue was caused by Chakra UI’s useBreakpointValue, which runs only on the client and is undefined during server-side rendering.

Resolution

I delayed rendering the header until after the component mounted.
This prevented layout shifts by ensuring the component only renders once the viewport width is available on the client. While this approach worked for my small portfolio site, it’s not ideal for larger applications. It serves as a temporary workaround until I find a more permanent and robust solution that handles responsive behavior more gracefully in a server-rendered environment.

const [hasMounted, setHasMounted] = useState(false);

useEffect(() => {
  setHasMounted(true);
}, []);

if (!hasMounted) return null;
Enter fullscreen mode Exit fullscreen mode

3. Improper Nesting of <html> and <body> Tags in Multiple Layouts

While structuring localized routes using app/[lng]/layout.js, I mistakenly returned a full HTML structure (including <html> and <body>) in both the root and localized layout files. This caused several critical errors, including:

Error: You are mounting a new html component when a previous one has not first unmounted.

And:

In HTML, <html> cannot be a child of <body>. This will cause a hydration error.

<body> cannot contain a nested <html>.

Resolution

Only the root app/layout.js should return the full HTML document structure. Nested layouts must return just the content portion.

// In app/layout.js
return (
  <html lang="en">
    <body>{children}</body>
  </html>
);

// In app/[lng]/layout.js
return (
  <main>{children}</main> // No html/body here
);
Enter fullscreen mode Exit fullscreen mode

4. Sticky Header Not Working Due to Global CSS

Although my header component used position="sticky" with Chakra UI, it failed to remain fixed during scroll. This behavior was inconsistent with how it worked in my previous CRA-based setup.

Root Cause

The default globals.css generated by create-next-app included the following rule:

body {
  max-width: 100vw;
  overflow-x: hidden;
}
Enter fullscreen mode Exit fullscreen mode

While this was intended to prevent horizontal scrolling, it unintentionally interfered with position: sticky by affecting the scroll context.

Resolution

Replacing overflow-x: hidden with the safer overflow-x: clip resolved the issue:

body {
  overflow-x: clip;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Migrating to Next.js with the App Router revealed several differences in rendering behavior compared to Create React App (CRA). Issues related to hydration, responsive layout handling, global styles, and layout structuring required careful debugging and thoughtful adjustments to the application's architecture.

Despite the hurdles, the migration was worthwhile for the benefits it brings in scalability, routing flexibility, and performance optimization. For anyone undertaking a similar migration, I recommend paying close attention to hydration warnings, how responsive logic behaves in server-rendered apps, and the broader impact of global CSS on layout behavior.

Top comments (0)