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>
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;
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
);
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;
}
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;
}
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)