For those interested, I was able to create a working demo.
https://hybrid-next-react.vercel.app/
https://github.com/coloradocolby/hybrid-next-react
The way it works is fairly simple. It's a basic Next.js application with a catch all route of [...app].js which serves the entry point to the React SPA. A simple isMounted variable (could easily be refactored into a hook) ensures that the React SPA (which likely has browser-specific code) is never processed by the server.
import { useEffect, useState } from "react";
import CreateReactAppRoot from "../src/App";
const App = () => {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
// Don't render client-side app unless window is available.
// NOTE: Page not statically pre-rendered nor server-side renderable
return isMounted ? <CreateReactAppRoot /> : null;
};
export default App;
The final step is to make sure you implement full page reloads from the React SPA to any route intended for Next.js. This ensures the Next.js routing mechanism is fired. In the demo above, I created both Next.js and React specific navbars.
import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";
export const NextNavbar = () => {
const router = useRouter();
return (
<nav>
{!router.pathname.includes("[...app]") && (
<ul className="flex p-4">
<li className="mr-3">
<Link href="/" passHref>
<div className="inline-block px-4 py-2 rounded cursor-pointer">
Home
</div>
</Link>
</li>
...
</ul>
)}
</nav>
);
};
Once again, although Next.js can intelligently link to the React SPA because of our catch all route, the React SPA requires a full page reload to any route intended for Next.js. As you might expect, if the destination route is meant to remain inside the React SPA, this is not necessary.
import React from "react";
import { Link } from "react-router-dom";
export const ReactNavbar = () => (
<nav>
<ul className="flex p-4">
<li className="mr-3">
<a href="/">
<div className="inline-block px-4 py-2 rounded cursor-pointer">
Home
</div>
</a>
</li>
...
<li className="mr-3">
<Link to="/faq">
<div className="inline-block px-4 py-2 rounded cursor-pointer">
FAQ
</div>
</Link>
</li>
...
</ul>
</nav>
);
One important caveat with this design is that it will not work if the CreateReactApp has global css imports, as Next.js requires all global styles to be imported in the _app.js file. From their documentation:
Due to the global nature of stylesheets, and to avoid conflicts, you may only import them inside pages/_app.js.
I imagine you could get around this by serving the Next.js and React SPA on different hosts and proxying between the two (NOTE: this would require custom external routing in the catch-all route), but it may be more trouble than it's worth. The easier solution may just be to refactor your global CSS into CSS modules.