Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Replacing Create React App with the Next.js CLI

Replacing Create React App with the Next.js CLI

How dare you make a jab at Create React App!?

Firstly, Create React App is good. But it's a very rigid CLI, primarily designed for projects that require very little to no configuration. This makes it great for beginners and simple projects but unfortunately, this means that it's pretty non-extensible. Despite the involvement from big names and a ton of great devs, it has left me wanting a much better developer experience with a lot more polish when it comes to hot reloading, babel configuration, webpack configuration, etc. It's definitely simple and good, but not amazing.

Now, compare that experience to Next.js which for starters has a much larger team behind it provided by a world-class company (Vercel) who are all financially dedicated to making it the best DX you could imagine to build any React application. Next.js is the ๐Ÿ’ฃ-diggity. It has amazing docs, great support, can grow with your requirements into SSR or static site generation, etc.

So why aren't more people using Next.js to build single-page-apps?

Next.js apps are normally tethered to its runtime framework. It's expected that you handle navigation with their next/router and next/link components and use their framework to build SSR-enabled apps out of the gate. Don't get me wrong, this is amazing, and if I'm building a website (not an app that probably sits behind a login), then I'll definitely go the normal route (pun intended) and stick with the Next.js runtime framework.

But if you're building a true single page app experience, you may not want to fudge with the Next.js runtime at all and just build as you would with CRA, but still get the amazing CLI and build experience that Next offers.

If that's you... then FINALLY I can tell yo how simple it is to achieve this:

How?

  • Remove react-scripts and any related scripts from your build
  • Go through the Getting Started - Manual Setup guide for Next.js
  • Add the .next directory to your .gitignore file
  • Rewrite all routes to be handled by pages/index.js in next.config.js.
module.exports = {
  target: 'serverless',
  async rewrites() {
    return [
      // Do not rewrite API routes
      {
        source: '/api/:any*',
        destination: '/api/:any*',
      },
      // Rewrite everything else to use `pages/index`
      {
        source: '/:any*',
        destination: '/',
      },
    ]
  },
}
  • Export your default App component in pages/index.js
export default function App() {
  return <div>Hello Next!</div>
}
  • Add React Router
    • npm install react-router-dom history
import React from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'

export default function App() {
  if (typeof document === 'undefined') {
    return null
  }

  return (
    <BrowserRouter>
      <Routes>
        <Route
          path="/"
          element={
            <>
              <h1>Welcome!</h1>
            </>
          }
        />
      </Routes>
    </BrowserRouter>
  )
}
  • Suppress Hydration Warnings

Rendering null on the server and not on the client will produce a hydration mismatch warning. Since, in this case, that's expected, we can silence it with a quick wrapper component that will supress the warning:

function SafeHydrate({ children }) {
  return (
    <div suppressHydrationWarning> // Must be a div, can't be a fragment ๐Ÿ˜‘๐Ÿคฆโ€โ™‚๏ธ
      {typeof document === 'undefined' ? null : children}
    </div>
  )
}

export default function App() {
  return (
    <SafeHydrate>
      <BrowserRouter>
        <Routes>
          <Route
            path="/"
            element={
              <>
                <h1>Welcome!</h1>
              </>
            }
          />
        </Routes>
      </BrowserRouter>
    </SafeHydrate>
  )
}

And on and on...

Follow Next.js docs, examples and guides to setup/migrate any other functionality or libraries.

@NZainchkovskiy

This comment has been minimized.

Copy link

@NZainchkovskiy NZainchkovskiy commented Sep 22, 2020

I think you have a typo in 4.
Should be module.exports, not module.export.

Thank you for this guide, it's brilliant!

@trickydisco78

This comment has been minimized.

Copy link

@trickydisco78 trickydisco78 commented Sep 28, 2020

What is the advantage of using next.js for SPA instead o CRA? Is it easier to configure and set up routing?

@ivan-dalmet

This comment has been minimized.

Copy link

@ivan-dalmet ivan-dalmet commented Sep 28, 2020

@tannerlinsley Thanks for this!
Just a quick question, how do you use react-router v6 with NextJS?
I can't find any documentation about it. And I'm getting ReferenceError: document is not defined (of course because Next is trying to read it on the "server" side during the dev)

@danestves

This comment has been minimized.

Copy link

@danestves danestves commented Oct 9, 2020

@ivan-dalmet you cannot use react router in nextjs, nextjs has his own router inside

@tannerlinsley

This comment has been minimized.

Copy link
Owner Author

@tannerlinsley tannerlinsley commented Oct 9, 2020

@danestves, you can and I do all the time! @ivan-dalmet I'll update the the guide to show that as an optional step.

@TheAlemazing

This comment has been minimized.

Copy link

@TheAlemazing TheAlemazing commented Oct 10, 2020

Brilliant!

@dpyzo0o

This comment has been minimized.

Copy link

@dpyzo0o dpyzo0o commented Oct 10, 2020

@tannerlinsley What if I would like to opt-in SSR and SSG later on? For example I need to add a public page /about which uses the SSG feature, do I need to change the RR6 configuration?

@ivan-dalmet

This comment has been minimized.

Copy link

@ivan-dalmet ivan-dalmet commented Oct 10, 2020

@tannerlinsley I found how to remove the warning ๐Ÿค—

import React from 'react';
import Head from 'next/head';
import { isBrowser } from '@/utils/ssr';
import { App } from '@/app/App';

const Index = () => {
  return (
    <>
      <Head>
        <title>Start UI</title>
      </Head>
      <div suppressHydrationWarning={true}>{isBrowser && <App />}</div>
    </>
  );
};
export default Index;
@ivan-dalmet

This comment has been minimized.

Copy link

@ivan-dalmet ivan-dalmet commented Oct 10, 2020

@dpyzo0o I think it's possible to use SSR for some pages but right now there is a bug on React Router v6 beta with basename ๐Ÿ˜•
ReactTraining/react-router#7216
A PR is open but it was not merged since a long time ๐Ÿ˜•
ReactTraining/react-router#7462

@dpyzo0o

This comment has been minimized.

Copy link

@dpyzo0o dpyzo0o commented Oct 10, 2020

@ivan-dalmet What I mean is to leverage the power of NextJS's SSR and SSG, e.g getServerSideProps and getStaticProps, which requires NextJS's file-system based routing. I wonder how would rr6 coexist with NextJS's file-system based routing.

What I can think of is to skip the routes which require NextJS's SSR and SSG in the rewrites config.

@ivan-dalmet

This comment has been minimized.

Copy link

@ivan-dalmet ivan-dalmet commented Oct 10, 2020

Yes my idea is to move the React Router v6 app at /app/* and rewrite on next config for this path only. BUt for that I need RR6 to handle a basename of /app and that is currently not working ๐Ÿ˜ข
I tried to rollback to RR5 but there are some more issue with search query params that not working well in Nextjs context

@tafelito

This comment has been minimized.

Copy link

@tafelito tafelito commented Oct 10, 2020

I'm curios as well about the benefits of this approach. I understand about the CLI but aren't you still using next runtime by installing next package? Are you only replacing next/router with react-router?

if you have to hide server side render errors, doesn't that mean you're still using SSR?

@tannerlinsley

This comment has been minimized.

Copy link
Owner Author

@tannerlinsley tannerlinsley commented Oct 11, 2020

@ivan-dalmet Updated!

@NMinhNguyen

This comment has been minimized.

Copy link

@NMinhNguyen NMinhNguyen commented Oct 11, 2020

Yes my idea is to move the React Router v6 app at /app/* and rewrite on next config for this path only. BUt for that I need RR6 to handle a basename of /app and that is currently not working ๐Ÿ˜ข
I tried to rollback to RR5 but there are some more issue with search query params that not working well in Nextjs context

You can use https://github.com/ds300/patch-package to temporarily apply the diff from that PR to your node_modules. Or if you're using Yarn v2, https://yarnpkg.com/features/protocols#patch

@jeetparikh

This comment has been minimized.

Copy link

@jeetparikh jeetparikh commented Oct 11, 2020

hey @tannerlinsley @ivan-dalmet I have got the 'index.tsx' file with this setup as described above

<SafeHydrate>
      <QueryClientProvider client={client}>
        <BrowserRouter>
          <Routes>
            <Route
              path="/about"
              element={
                <>
                  <h1>About!</h1>
                </>
              }
            />
            <Route
              path="/users"
              element={
                <>
                  <h1>Users!</h1>
                </>
              }
            />
            <Route
              path="/"
              element={
                <>
                  <h1>Welcome!</h1>
                </>
              }
            />
          </Routes>
        </BrowserRouter>
      </QueryClientProvider>
    </SafeHydrate>

But when I try to go from '/' to '/about' the entire page reloads instead of just the component changing - why is this happening?
under pages do I need the _app.tsx? and where should the index.tsx sit - under pages? or the pages folder shouldn't be in place anymore? thanks for your help

@tannerlinsley

This comment has been minimized.

Copy link
Owner Author

@tannerlinsley tannerlinsley commented Oct 12, 2020

@jeetparikh, are you using React Router's <Link to='/about'> component?

@jeetparikh

This comment has been minimized.

Copy link

@jeetparikh jeetparikh commented Oct 12, 2020

@tannerlinsley good point.. No I was just trying to access it via the browser and I thought it would work the same. But with Link it does.. Thanks for pointing out..
Just curious why it wouldn't work directly in browser URL? It does for my Angular apps?
thank you for your response.

@tannerlinsley

This comment has been minimized.

Copy link
Owner Author

@tannerlinsley tannerlinsley commented Oct 12, 2020

@jeetparikh, it should work just fine in the browser, but it will reload the page when you hit enter in your URL bar. If it doesn't do that in other apps, it means you're using hashbang router eg. https://site.com/#/about. Hashes don't cause reloads when you manually edit the URL in the browser.

@jeetparikh

This comment has been minimized.

Copy link

@jeetparikh jeetparikh commented Oct 12, 2020

@tannerlinsley yes makes sense.. Its the '#' that makes the difference - missed that.. thanks :)

@jeetparikh

This comment has been minimized.

Copy link

@jeetparikh jeetparikh commented Oct 12, 2020

hey @tannerlinsley. The default @nrwl/next project creates a _app.ts in pages directory along with index.ts. With this approach should I still be keeping the _app.ts file and index.ts? Cos I was trying to configure my tailwindcss and I get this error when trying to import the .css file in index.ts

error - ./styles/index.css
Global CSS cannot be imported from files other than your Custom <App>. Please move all global CSS imports to pages/_app.js. Or convert the import to Component-Level CSS (CSS Modules).
Read more: https://err.sh/next.js/css-global
Location: pages/index.tsx
Could not find files for / in .next/build-manifest.json

And if I wanted to keep the _app.ts does the next.config.js above change?
Thanks. Appreciate your help ๐Ÿ‘

@tannerlinsley

This comment has been minimized.

Copy link
Owner Author

@tannerlinsley tannerlinsley commented Oct 12, 2020

@jeetparikh

This comment has been minimized.

Copy link

@jeetparikh jeetparikh commented Oct 12, 2020

@tannerlinsley works fine. thanks :)

@jeetparikh

This comment has been minimized.

Copy link

@jeetparikh jeetparikh commented Oct 17, 2020

hey @a-eid you need to do exactly as the error says.. you need to import the global css file inside pages/_app.js and it works fine. I do that too.. or nextjs supports module css files import in the format filename.module.css..
check this out https://nextjs.org/docs/basic-features/built-in-css-support

@jeetparikh

This comment has been minimized.

Copy link

@jeetparikh jeetparikh commented Oct 17, 2020

hey @tannerlinsley @ivan-dalmet everything seemed to work fine with the suppression warning. But recently I installed Pixijs library and tried importing from it and started getting this error

ReferenceError: window is not defined
    at Object.<anonymous> (/Users/jeetparikh/development/farmhand/node_modules/@pixi/polyfill/lib/polyfill.js:16:1)
    at Module._compile (internal/modules/cjs/loader.js:778:30)

where the lib is trying to access some stuff on the window object - any help to fix this please?
Thank you for your help.

@NZainchkovskiy

This comment has been minimized.

Copy link

@NZainchkovskiy NZainchkovskiy commented Oct 17, 2020

can't seem to be able to use next env variable ? .env.local doesn't work at all.

I used .env.production 10 minutes ago and it's just works.

@NZainchkovskiy

This comment has been minimized.

Copy link

@NZainchkovskiy NZainchkovskiy commented Oct 17, 2020

hey @tannerlinsley @ivan-dalmet everything seemed to work fine with the suppression warning. But recently I installed Pixijs library and tried importing from it and started getting this error

ReferenceError: window is not defined
    at Object.<anonymous> (/Users/jeetparikh/development/farmhand/node_modules/@pixi/polyfill/lib/polyfill.js:16:1)
    at Module._compile (internal/modules/cjs/loader.js:778:30)

where the lib is trying to access some stuff on the window object - any help to fix this please?
Thank you for your help.

I got similar errors with other libraries. When I started to use Dynamic Component with ssr:false, errors disappeared.

@jeetparikh

This comment has been minimized.

Copy link

@jeetparikh jeetparikh commented Oct 17, 2020

hey @NZainchkovskiy could you please elaborate a bit? When you say dynamic component with ssr:false please? Any links would be appreciated or sample code? Thanks

@jeetparikh

This comment has been minimized.

Copy link

@jeetparikh jeetparikh commented Oct 18, 2020

thanks @NZainchkovskiy found it.. works fine

@brookslybrand

This comment has been minimized.

Copy link

@brookslybrand brookslybrand commented Oct 20, 2020

Yes my idea is to move the React Router v6 app at /app/* and rewrite on next config for this path only. BUt for that I need RR6 to handle a basename of /app and that is currently not working ๐Ÿ˜ข
I tried to rollback to RR5 but there are some more issue with search query params that not working well in Nextjs context

@ivan-dalmet I believe you can just add basename='/app' to the root <Routes /> component, that worked for me, I might be missing something though

EDIT: I read ReactTraining/react-router#7216. I see there are some issues. This at least got things working, but there are definitely some confusing cases. I guess I'll roll with just having a <Routes basename='/app'> right underneath the <BrowserRouter> component and have everything else nest underneath that. I think that should work until they merge in the fix

@eddyw

This comment has been minimized.

Copy link

@eddyw eddyw commented Oct 22, 2020

Someone shared with me a link to this gist, while I like the idea, I think you lose a lot of benefits from NextJS using this approach. I was working on an alternative and I finally managed to put it in a CodeSandbox (uses React Router v6 beta), if anyone interested: https://codesandbox.io/s/nextjs-react-router-md4ur?file=/pages/%5B%5B...index%5D%5D.tsx

What it basically does is to render a custom _app and render the page using StaticRouter on server but I create a custom Router for the browser. It's a custom MemoryRouter so we don't have two browser history managers (react router and NextJS). Then the trick is to listen to history events (history.listen) for PUSH and REPLACE actions, so then we can dispatch this same events using the NextJS router. This is the piece of code that does all the magic:

			const listener = ({ action, location }) => {
				const url = {
					hash: location.hash,
					pathname: location.pathname,
					search: location.search,
				}
				const shallow = Boolean(location.state?.shallow)
				switch (action) {
					case 'PUSH':
						return NextRouter.push(url, void 0, { shallow })
					case 'REPLACE':
						return NextRouter.replace(url, void 0, { shallow })
					default:
						return void 0
				}
			}
			history.listen(listener)

This way you can use React Router however you want but under the hood history events are managed by NextJS. This has the benefit that you aren't limited to an SPA (or single file) but you could (optionally) still use NextJS file system router .. with React Router and have some pages as static, with getInitialProps, or getServerSideProps (example in CodeSandBox)

@tannerlinsley

This comment has been minimized.

Copy link
Owner Author

@tannerlinsley tannerlinsley commented Oct 22, 2020

Yeah this example never claimed any support for SSR. It's clearly just a way to be rid of CRA (which is also non-SSR) and use Next.js's better CLI.

@colinhacks

This comment has been minimized.

Copy link

@colinhacks colinhacks commented Oct 22, 2020

@tannerlinsley instead of rewriting all the URLs, could you just use an optional catchall route? https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes

Basically pages/index.tsx => pages/[[...index]].tsx?

[EDIT: you can't, see my comment below]

@tafelito

This comment has been minimized.

Copy link

@tafelito tafelito commented Oct 26, 2020

did anyone have an issue when using useParams from RR v5? everything works fine in dev but when I do the build I get
Error occurred prerendering page "/...". Read more: https://err.sh/next.js/prerender-error TypeError: Cannot read property 'match' of undefined
This is not an issue with RR itself, it happens with any context provider defined in index.js. Same issue happened with mobx-state-tree and I had to move it to the _app.js file. But if I move the BrowserRouter to _app, then there is this error Error: Invariant failed: Browser history needs a DOM

@colinhacks

This comment has been minimized.

Copy link

@colinhacks colinhacks commented Oct 29, 2020

Additional details

  1. You don't need the first rewrite rule for /api/:any*. Next.js doesn't apply rewrites if a route matches an existing page in your /pages directory:
    Screen Shot 2020-10-30 at 3 12 34 PM

  2. There's no need to define target: "serverless" manually. All deployments to Vercel are automatically made serverless. [EDIT: This is true but if you're deploying your app somewhere other than Vercel then you still need to set this explicitly!]

  3. Moving SafeHydrate into a custom app (/pages/_app.tsx) is a more idiomatic approach. Then you can implement your React Router in /pages/index.tsx without a wrapper component.

I wrote up a more detailed version of this approach here: Building a single-page application with Next.js. I try to provide more context around the errors being resolved and why this works. My post also divorces the discussion from Create React App, instead providing a general approach for building a SPA on top of create-next-app. ๐Ÿ‘

@tafelito

This comment has been minimized.

Copy link

@tafelito tafelito commented Oct 30, 2020

Basically pages/index.tsx => pages/[[...index]].tsx?

@vriad that might only works if you deploy to vercel. If you're planning to deploy to something else, like render, then you have to manually set up the rewrites, and you can't use the next.config.js

Oh and as you pointed out in your post, I had to move the SafeHydrate component to the _app.tsx instead and that got rid of the error I had above

@colinhacks

This comment has been minimized.

Copy link

@colinhacks colinhacks commented Oct 30, 2020

@tafelito good call! After trying to get catch-all routes to work I realized the problem. It's pretty subtle!

Screen Shot 2020-10-30 at 3 16 55 PM

@dpyzo0o

This comment has been minimized.

Copy link

@dpyzo0o dpyzo0o commented Nov 4, 2020

@eddyw Hi, your approach looks really interesting. I just have some questions regarding you approach

  1. What's the difference between react-router's Link component and nextjs' Link component in this setup? From the sandbox I see them behave the same.
  2. Seems like your approach is actually not SPA but still server rendered by Nextjs. Does that mean you can only deploy it in a node environment? As for the approach in this gist, you can actually next export and host it as static files.
  3. I see your point is to also use the file-system routing of nextjs. However, with the setup in this gist, you could also just use the file-system routing because Next.js doesn't apply rewrites if a route matches an existing page in your /pages directory.
@eddyw

This comment has been minimized.

Copy link

@eddyw eddyw commented Nov 4, 2020

@dpyzo0o ๐Ÿ‘‹

  1. The next/link component implements an intersection observer. When a link is visible in the page or you mouseover the link, it'll prefetch the page. However, with react-router Link, it doesn't prefetch the page. For instance, by just moving the mouse over the Link (of next/link), you can see in devtools network panel that a request is made to prefetch the page.

  2. You can still export server-side rendered apps with next export using my approach. In the CodeSandbox I used getServerSideProps just for demonstration but you can completely get away without it

  3. Good point ๐Ÿ‘ but because the gist forces not to SSR on the _app, other pages won't be SSR-ed ๐Ÿ˜…

My CodeSandbox was out of date with I was locally working on. I took a slightly different approach by creating a compatible history instance with react-router and I updated the [[...index]] file to reflect how it'd look like if you were to do a next export

https://codesandbox.io/s/nextjs-react-router-md4ur?file=/pages/%5B%5B...index%5D%5D.tsx

@dpyzo0o

This comment has been minimized.

Copy link

@dpyzo0o dpyzo0o commented Nov 4, 2020

@eddyw Cool! I will wrap my head and try to understand what's going on in this sandbox. I really like the way you approach it, looks very smart!

@johot

This comment has been minimized.

Copy link

@johot johot commented Nov 10, 2020

Thank you @tannerlinsley and @vriad! Awesome information!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You canโ€™t perform that action at this time.