Skip to content

Instantly share code, notes, and snippets.

@tannerlinsley
Last active April 12, 2024 17:04
Show Gist options
  • Save tannerlinsley/65ac1f0175d79d19762cf06650707830 to your computer and use it in GitHub Desktop.
Save tannerlinsley/65ac1f0175d79d19762cf06650707830 to your computer and use it in GitHub Desktop.
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.

@dpyzo0o
Copy link

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
Copy link

johot commented Nov 10, 2020

Thank you @tannerlinsley and @vriad! Awesome information!

@pranaypratyush
Copy link

What's the difference between this method and simply importing my main App component with next/dynamic and passing {ssr: false}?

@devinrhode2
Copy link

Would it be fair to say that if you are going to migrate to next.js, you're better off also migrating to next router? Idk, I really love React router and am excited for v6, then again next has such great features..

@tannerlinsley
Copy link
Author

Depends if you need need dynamic routing with nested layout

@webdevike
Copy link

webdevike commented Mar 1, 2021

Is there a way to handle 404 routes with this setup?
This doesn't seem to work.
<Route path="*"> <GenericNotFound /> </Route>

@lasota-piotr
Copy link

lasota-piotr commented Mar 2, 2021

Not found page works for me:

import React from 'react';
import getConfig from 'next/config';
import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom';

export function Index() {
  return (
    <SafeHydrate>
      <Router>
        <div>
          <nav>
            <ul>
              <li>
                <Link to="/">Home</Link>
              </li>
              <li>
                <Link to="/article">article</Link>
              </li>
              <li>
                <Link to="/listing">listing</Link>
              </li>
            </ul>
          </nav>
        </div>
        <Switch>
          <Route exact path="/">
            hello from next app
          </Route>
          <Route exact path="/article">
            <Article />
          </Route>
          <Route exact path="/listing">
            <Listing />
          </Route>
          <Route path="*">
            <div>Not found</div>
          </Route>
        </Switch>
      </Router>
    </SafeHydrate>
  );
}

@eddyw
Copy link

eddyw commented Mar 2, 2021

Is there a way to handle 404 routes with this setup?
This doesn't seem to work.
<Route path="*"> <GenericNotFound /> </Route>

You can't have status 404 set with this setup. You could use next Head to set <meta name="robots" content="noindex" /> which would the closet thing to prevent indexing of ""404"" pages.

@lasota-piotr
Copy link

I think this setup is for apps that sit behind a login page, so they aren't available for robots.

@domhede
Copy link

domhede commented Mar 11, 2021

I have a question about the exported output. I noticed that any component in the pages directory gets a html file generated by next build, there are also static js chunks generated for each "page". I was expecting to only need the index.html. I'm pretty sure this would be required for SSR. As we are opted out of SSR I'm wondering how I can configure to not generate these artefacts?

Ideally I'd like to be able to group components together within /pages.
ie.

/pages
  /profile
    Profile.tsx 
    Profile.graphql
    Profile.test.tsx
    /AnotherComponent
      AnotherComponent.tsx
      [...]

@DavidWells
Copy link

I set up a repo using @eddyw's example where react-router links and next links can be used interchangeably

https://next-with-react-router.netlify.app/ + https://github.com/DavidWells/next-with-react-router-v6

This approach works pretty well and you can statically generate pages or use serverside pages.

@leerob
Copy link

leerob commented May 12, 2021

FYI target isn't needed anymore, might want to update and remove that 👍

@abadfish
Copy link

Anyone know why I get this when using Routes but works when using Switch?

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

Check the render method of `App`.

Broken:

import React from 'react'
import { BrowserRouter as Router, Route, Switch, Routes } from 'react-router-dom'
import Home from '../src/views/Home'

function SafeHydrate({ children }) {
  return (
    <div suppressHydrationWarning> 
      {typeof document === 'undefined' ? null : children}
    </div>
  )
}

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

Works:

import React from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import Home from '../src/views/Home'

function SafeHydrate({ children }) {
  return (
    <div suppressHydrationWarning> 
      {typeof document === 'undefined' ? null : children}
    </div>
  )
}

export default function App() {
  return (
    <SafeHydrate>
      <Router>
        <Switch>
          <Route exact path="/" component={ Home }/>
        </Switch>
      </Router>
    </SafeHydrate>
  )
}

The second one is how I write an App.js with CRA but just curious why Routes doesn't work and Switch does.

@colinhacks
Copy link

I don't think Routes is a thing in react-router-dom

@abadfish
Copy link

Yes, @colinhacks, it looks like that's true. I didn't catch that so thanks. react-router-dom is what @tannerlinsley uses in the guide and imports Routes. Wonder why this hasn't come up before?

@lasota-piotr
Copy link

I think, he uses react router 6, not 5

@colinhacks
Copy link

Yes, @colinhacks, it looks like that's true. I didn't catch that so thanks. react-router-dom is what @tannerlinsley uses in the guide and imports Routes. Wonder why this hasn't come up before?

Good question, very weird 🤔 Probably because everyone else was using TypeScript and caught the problem immediately 😜

But even weirder Routes is now a valid construct in react-router v6 as @lasota-piotr pointed out (but this gist hasn't been updated since Oct 2020: https://gist.github.com/tannerlinsley/65ac1f0175d79d19762cf06650707830/revisions)

@abadfish
Copy link

You insufferable TypeScript people lol
Here's a repo using both next/link and react-router-dom. Super simple and maybe too early to tell what kind of problems I'll run into once I start adding more data connections but my endgame is to have a mix of SSR for some pages and client side render for others. Totally open to criticism.. I'm new to NextJS and this is just a tinker repo.
Also using React-Query in there to add to the mix. Feel free to fork.
Client: https://github.com/abadfish/next-as-build-exp
API: https://github.com/abadfish/exp-api (just to give it some data/endpoints to query)

@trangcongthanh
Copy link

Yes, @colinhacks, it looks like that's true. I didn't catch that so thanks. react-router-dom is what @tannerlinsley uses in the guide and imports Routes. Wonder why this hasn't come up before?

Good question, very weird 🤔 Probably because everyone else was using TypeScript and caught the problem immediately 😜

But even weirder Routes is now a valid construct in react-router v6 as @lasota-piotr pointed out (but this gist hasn't been updated since Oct 2020: https://gist.github.com/tannerlinsley/65ac1f0175d79d19762cf06650707830/revisions)

Why is weird? Michael Jackson announced React Router v6 alpha on Apr 7th, 2020. https://reacttraining.com/blog/react-router-v6-pre

@simplenotezy
Copy link

Would it be possible to still use Next.js router, instead of using Reacts Router, so you could leverage Next.js pages/ structure? That's the question I'm fiddling with at the moment.

For instance, when you deploy your site, you'll have any incoming request point to [[..app.js]] and then perhaps Next.js router will catch and serve the proper page?

@effinrich
Copy link

You insufferable TypeScript people lol
Here's a repo using both next/link and react-router-dom. Super simple and maybe too early to tell what kind of problems I'll run into once I start adding more data connections but my endgame is to have a mix of SSR for some pages and client side render for others. Totally open to criticism.. I'm new to NextJS and this is just a tinker repo.
Also using React-Query in there to add to the mix. Feel free to fork.
Client: https://github.com/abadfish/next-as-build-exp
API: https://github.com/abadfish/exp-api (just to give it some data/endpoints to query)

This is very cool and using react-query in this context is helpful as I want to use it as well. I noticed you have your entire node_modules directory built in your repo. You might want to wipe that out as attempting to clone your repo will be quite a task I think.

@abadfish
Copy link

@effinrich you're right. My bad. Deleted.

@sean-esper
Copy link

sean-esper commented Oct 29, 2021

The blocker we ran into was how Next.js enforces how 3rd party libraries can use css. It appears to be completely incompatible with things like Cypress and CKEditor. The suggestion Next.js give to fix it is "Reach out to the maintainers and have them release a compiled version"

There are small things we use, that are largely unmaintained like react-spinkit, that I do not expect to go back and fix this for us.. and I dont really want to go open a dozen issues/pull requests to make our codebase compatible.

There is an RFC out to fix, but no sign on when it will be incorportated. Does anyone else run into this and have a workaround? I think its pretty restrictive to define how third parties can use css in their modules before they can be used in any Next.js app...

RFC for context: vercel/next.js#27953

@leerob
Copy link

leerob commented Oct 30, 2021

The suggestion Next.js give to fix it is "Reach out to the maintainers and have them release a compiled version

We don't believe this is productive, which is why we opened the RFC mentioned.

I think its pretty restrictive to define how third parties can use css in their modules before they can be used in any Next.js app

Please read the RFC – It's very nuanced. Ultimately, the ship has sailed it's something we want to support for the best ecosystem compat.

We still plan on implementing this! 🙏

@sean-esper
Copy link

sean-esper commented Oct 30, 2021 via email

@benoror
Copy link

benoror commented May 13, 2022

Looks like Next rewrites have changed from :any* to :slug*: https://nextjs.org/docs/api-reference/next.config.js/rewrites

@geekyme-fsmk
Copy link

geekyme-fsmk commented Jun 11, 2022

On react 18, the hydration errors would still appear. They only go away when downgraded to react 17. @tannerlinsley

Alternatively, could use this:

function ClientSideRendering({ children }: any) {
  const [csrReady, setCsrReady] = useState(false);
  useEffect(() => {
    setCsrReady(true);
  });

  return csrReady ? children : null;
}

export default function App() {
  return (
    <ClientSideRendering>
      <BrowserRouter>
        <div>
         .......
        </div>
      </BrowserRouter>
    </ClientSideRendering>
  );
}

@johnoscott
Copy link

johnoscott commented Jul 19, 2022

@geekyme-fsmk I like your solution, however there are redundant re-renders when calling useEffect without any dependencies. Instead, calling setCsrReady(true) once is enough with the 'one-shot' pattern of useEffect with an empty dependancy array.

To see this working, I have added console.log()s which can be viewed in BOTH the nextJS server logs and the browser console.

You can remove the RENDER_COUNT logic and console.log()s once you are convinced (like I did) that the logic works.

import { useEffect, useState } from "react";
import ReactLocationQueryApp from "./ReactLocationQueryApp";

let RENDER_COUNT = 0;

// renders child components ONLY if running client-side
function ClientSideRendering({ children }: any) {

    RENDER_COUNT++;

    const [csrReady, setCsrReady] = useState(false);

    // NOTE: this will NEVER fire on Server Side, but it will (of course) run in the Browser
    useEffect(() => {

        console.log(`ClientSideRendering: 🔵 useEffect: RENDER_COUNT=${ RENDER_COUNT }, csrReady=${ csrReady }`)

        setCsrReady(true); 

    },[]); // one-shot function. ie. will call ONLY once

    console.log(`ClientSideRendering: RENDER_COUNT=${ RENDER_COUNT }, csrReady=${ csrReady }` +
        `${ csrReady ? '🟢' : '❌' }`
    );

    return csrReady ? children : null;
}

// Intended to ONLY run client-side (as a SPA - Single Page Application) in React 18+ / NextJS 12+
// ala "SPA Mode"
export default function ClientSideApp() {
    return (
        <ClientSideRendering>
            <ReactLocationQueryApp/>
        </ClientSideRendering>
    )
}

@Mister-Zeng
Copy link

Anyone know a fix to this error?

  • error node_modules/next/dist/pages/_app.js (12:0) @ eval
  • error Error [TypeError]: interop_require_default. is not a function
    at eval (webpack-internal:///./node_modules/next/dist/pages/_app.js:12:55)
    at ./node_modules/next/dist/pages/_app.js (/home/jzeng/repos/cps-react/.next/server/pages/_document.js:62:1)
    at webpack_require (/home/jzeng/repos/cps-react/.next/server/webpack-runtime.js:33:42)
    at eval (webpack-internal:///./node_modules/next/dist/build/webpack/loaders/next-route-loader/index.js?page=%2F_document&preferredRegion=&absolutePagePath=private-next-pages%2F_document&absoluteAppPath=private-next-pages%2F_app&absoluteDocumentPath=private-next-pages%2F_document&middlewareConfigBase64=e30%3D!:21:80)
    at ./node_modules/next/dist/build/webpack/loaders/next-route-loader/index.js?page=%2F_document&preferredRegion=&absolutePagePath=private-next-pages%2F_document&absoluteAppPath=private-next-pages%2F_app&absoluteDocumentPath=private-next-pages%2F_document&middlewareConfigBase64=e30%3D! (/home/jzeng/repos/cps-react/.next/server/pages/_document.js:32:1)
    at webpack_require (/home/jzeng/repos/cps-react/.next/server/webpack-runtime.js:33:42)
    at webpack_exec (/home/jzeng/repos/cps-react/.next/server/pages/_document.js:202:39)
    at /home/jzeng/repos/cps-react/.next/server/pages/_document.js:203:28
    at Object. (/home/jzeng/repos/cps-react/.next/server/pages/_document.js:206:3)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at Module.require (node:internal/modules/cjs/loader:1143:19)
    at require (node:internal/modules/cjs/helpers:110:18)
    at requirePage (/home/jzeng/repos/cps-react/node_modules/next/dist/server/require.js:112:75)
    at /home/jzeng/repos/cps-react/node_modules/next/dist/server/load-components.js:76:65
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Promise.all (index 0)
    at async loadComponentsImpl (/home/jzeng/repos/cps-react/node_modules/next/dist/server/load-components.js:75:33)
    at async DevServer.findPageComponentsImpl (/home/jzeng/repos/cps-react/node_modules/next/dist/server/next-server.js:772:36) {
    digest: undefined
    }
    null

@cduff
Copy link

cduff commented Oct 19, 2023

I recently migrated a large CRA SPA to Next.js. I suggest reviewing the following official Next.js documentation: https://nextjs.org/docs/app/building-your-application/upgrading/from-vite. It's for Vite>Next but mostly applies to CRA>Next also.

If anyone is using Next.js app router for an SPA and using some other client-side router like react-router-dom then the following issue & workaround is probably currently relevant to you: vercel/next.js#56636.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment