vercel / next.js

The React Framework
https://nextjs.org
MIT License
126.94k stars 26.98k forks source link

Using a nextjs from a custom server wrapper (Error: Invalid hook call) #20838

Closed alexhochreiter closed 3 years ago

alexhochreiter commented 3 years ago

What version of Next.js are you using?

10.0.4

What version of Node.js are you using?

v14.15.1

What browser are you using?

All

What operating system are you using?

Fedora Workstation 33

How are you deploying your application?

I cannot

Describe the Bug

I am using a wrapper to streamline my page structures across different projects, which utilizes nextjs's custom server. Yet the wrapper fails with "Error: Invalid hook call." when trying to render anything. I checked if different versions of react or react-dom are installed. npm ls react says react@17.0.1 is the only version installed.

resulting error stack:

[alex@localhost my-app]$ node index.js 
event - compiled successfully
Example app listening at http://localhost:8000
serving route /hello
event - build page: /next/dist/pages/_error
wait  - compiling...
event - compiled successfully
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
    at resolveDispatcher (/home/alex/tmp/twoprojects/my-app/node_modules/react/cjs/react.development.js:1476:13)
    at useContext (/home/alex/tmp/twoprojects/my-app/node_modules/react/cjs/react.development.js:1484:20)
    at Head (/home/alex/tmp/twoprojects/my-app/node_modules/next/dist/next-server/lib/head.js:15:66)
    at processChild (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/react-dom/cjs/react-dom-server.node.development.js:3353:14)
    at resolve (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/react-dom/cjs/react-dom-server.node.development.js:3270:5)
    at ReactDOMServerRenderer.render (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/react-dom/cjs/react-dom-server.node.development.js:3753:22)
    at ReactDOMServerRenderer.read (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/react-dom/cjs/react-dom-server.node.development.js:3690:29)
    at renderToString (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/react-dom/cjs/react-dom-server.node.development.js:4298:27)
    at Object.renderPage (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/next/dist/next-server/server/render.js:53:851)
    at Function.getInitialProps (webpack-internal:///../my-nextjswrapper-lib/node_modules/next/dist/pages/_document.js:135:19)
    at loadGetInitialProps (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/next/dist/next-server/lib/utils.js:5:101)
    at renderToHTML (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/next/dist/next-server/server/render.js:53:1142)
    at async /home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/next/dist/next-server/server/next-server.js:99:97
    at async /home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/next/dist/next-server/server/next-server.js:92:142
    at async DevServer.renderToHTMLWithComponents (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/next/dist/next-server/server/next-server.js:124:387)
    at async DevServer.renderErrorToHTML (/home/alex/tmp/twoprojects/my-nextjswrapper-lib/node_modules/next/dist/next-server/server/next-server.js:126:327)
^C

Expected Behavior

expected to serve a minimal example page without problems - doesnt matter if implemented as hook or not (the error message is the same in both cases)

To Reproduce

create two (empty) projects:

contents of my-nextjswrapper-lib/package.json

{
  "name": "my-nextjswrapper-lib",
  "version": "0.1.0",
  "private": true,
  "peerDependencies": {
    "next": "10.0.4",
    "react": "17.0.1",
    "react-dom": "17.0.1"
  }
}

contents of my-nextjswrapper-lib/index.js

const nextjs = require('next')
const path = require('path')

class MyNextjsWrapper {
  constructor (options) {
    this.options = options
    this.nextjsApp = null
  }

  render (path, req, res) {
    if (!this.nextjsApp) {
        throw new Error(`Cannot render nextJs page for path '${req.path}': .setup was not called yet! nextjs not ready`)
      } else {
        this.nextjsApp.render(req, res, path, req.query)
      }
  }

  async setup (expressApp) {
    // setup nextjs as usual
    this.nextjsApp = nextjs({
      dev: this.options.debug,
      conf: {
        useFileSystemPublicRoutes: false,
        poweredByHeader: false,
        // changing the webpack config to resolve
        // the multiple react instances/versions issue
        webpack: (config, { buildId, dev, resolve, isServer, defaultLoaders, webpack }) => {
          // Note: we provide webpack above so you should not `require` it
          /* config.resolve.alias = {
            react: path.resolve('./node_modules/react'),
            'react-dom': path.resolve('./node_modules/react-dom')
          } */
          /* config.externals = {
            react: 'React'
          } */
          return config
        }
      }
    })
    await this.nextjsApp.prepare().catch(error => {
      throw new Error(`Error prepating NextJS App instance: ${error}`)
    })
    this.nextjsHandle = this.nextjsApp.getRequestHandler()
    expressApp.get('/_next/*', (req, res) => {
      console.log(`nextjs is serving route: ${req.path}`)
      this.nextjsHandle(req, res, req.url)
    })
  }
}

module.exports = MyNextjsWrapper

contents of my-app/package.json

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "next": "10.0.4",
    "react": "17.0.1",
    "react-dom": "17.0.1"
  }
}

contents of my-app/index.js

const MyNextjsWrapper = require('my-nextjswrapper-lib')
const express = require('express')
const expressApp = express()
const myNextjsWrapper = new MyNextjsWrapper({ debug: true })
const port = 8000

const Main = async () => {
  await myNextjsWrapper.setup(expressApp)

  expressApp.get('/hello', (req, res) => {
    console.log('serving route /hello')
    myNextjsWrapper.render('/hello', req, res)
  })

  expressApp.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
  })
}

Main()

contents of my-app/pages/hello.js

const Hello = () => {
  return (
    <div>
      <p>Hello, i am the HELLO page</p>
    </div>
  )
}

export default Hello

To test locally, go to the my my-nextjswrapper-lib dir and execute npm link. Then go to the my-app dir and execute npm i then npm link my-nextjswrapper-lib. Start the server from the my-app dir with node index.js and open localhost:8000/hello from any browser.

eric-burel commented 3 years ago

Had similar issues in Vulcan and I scratched my head on this. Maybe check https://github.com/VulcanJS/vulcan-npm/blob/devel/scripts/link-duplicates.sh and https://github.com/VulcanJS/vulcan-next/blob/devel/scripts/link-vulcan.sh. Basically in this case I also link "react" and "react-dom" (don't forget to always check both when doing yarn why). They are also peer dependencies, so theoretically this should not be a problem, but I don't really get how this work internally. Since I still need to install react to test by NPM packages, it seems to be the version used by them instead of the Next project version => leads to bugs.

With those scripts I don't have problems anymore, I just run them at install first in the NPM monorepo then in the Next app.