diff --git a/packages/next-server/lib/router/router.js b/packages/next-server/lib/router/router.js
index ee48b2f5..82aa924b 100644
--- a/packages/next-server/lib/router/router.js
+++ b/packages/next-server/lib/router/router.js
@@ -260,8 +260,11 @@ export default class Router {
const { Component } = routeInfo
- if (typeof Component !== 'function') {
- throw new Error(`The default export is not a React Component in page: "${pathname}"`)
+ if (process.env.NODE_ENV !== 'production') {
+ const { isValidElementType } = require('react-is')
+ if (!isValidElementType(Component)) {
+ throw new Error(`The default export is not a React Component in page: "${pathname}"`)
+ }
}
const ctx = { pathname, query, asPath: as }
diff --git a/packages/next-server/package.json b/packages/next-server/package.json
index 355fc4f5..82957231 100644
--- a/packages/next-server/package.json
+++ b/packages/next-server/package.json
@@ -44,6 +44,7 @@
"@taskr/watch": "1.1.0",
"@types/react": "16.7.13",
"@types/react-dom": "16.0.11",
+ "@types/react-is": "16.5.0",
"@types/send": "0.14.4",
"taskr": "1.1.0",
"typescript": "3.1.6"
diff --git a/packages/next-server/server/render.tsx b/packages/next-server/server/render.tsx
index e16907a6..c328cf55 100644
--- a/packages/next-server/server/render.tsx
+++ b/packages/next-server/server/render.tsx
@@ -106,7 +106,7 @@ function renderDocument(Document: React.ComponentType, {
files={files}
dynamicImports={dynamicImports}
assetPrefix={assetPrefix}
- {...docProps}
+ {...docProps}
/>
)
}
@@ -131,9 +131,13 @@ export async function renderToHTML (req: IncomingMessage, res: ServerResponse, p
await Loadable.preloadAll() // Make sure all dynamic imports are loaded
- if (typeof Component !== 'function') {
- throw new Error(`The default export is not a React Component in page: "${pathname}"`)
+ if (dev) {
+ const { isValidElementType } = require('react-is')
+ if (!isValidElementType(Component)) {
+ throw new Error(`The default export is not a React Component in page: "${pathname}"`)
+ }
}
+
if (!Document.prototype || !Document.prototype.isReactComponent) throw new Error('_document.js is not exporting a React component')
const asPath = req.url
@@ -141,7 +145,7 @@ export async function renderToHTML (req: IncomingMessage, res: ServerResponse, p
const router = new Router(pathname, query, asPath)
const props = await loadGetInitialProps(App, {Component, router, ctx})
- // the response might be finshed on the getInitialProps call
+ // the response might be finished on the getInitialProps call
if (isResSent(res)) return null
const devFiles = buildManifest.devFiles
@@ -163,14 +167,14 @@ export async function renderToHTML (req: IncomingMessage, res: ServerResponse, p
return render(renderElementToString, )
}
- return render(renderElementToString,
+ return render(renderElementToString,
reactLoadableModules.push(moduleName)}>
-
+
)
}
diff --git a/packages/next/client/index.js b/packages/next/client/index.js
index 892bb935..737c42e1 100644
--- a/packages/next/client/index.js
+++ b/packages/next/client/index.js
@@ -83,8 +83,11 @@ export default async ({
try {
Component = await pageLoader.loadPage(page)
- if (typeof Component !== 'function') {
- throw new Error(`The default export is not a React Component in page: "${page}"`)
+ if (process.env.NODE_ENV !== 'production') {
+ const { isValidElementType } = require('react-is')
+ if (!isValidElementType(Component)) {
+ throw new Error(`The default export is not a React Component in page: "${page}"`)
+ }
}
} catch (error) {
// This catches errors like throwing in the top level of a module
diff --git a/packages/next/package.json b/packages/next/package.json
index 9912ee40..d6f494e8 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -75,6 +75,7 @@
"prop-types": "15.6.2",
"prop-types-exact": "1.2.0",
"react-error-overlay": "4.0.0",
+ "react-is": "16.6.3",
"recursive-copy": "2.0.6",
"resolve": "1.5.0",
"strip-ansi": "3.0.1",
diff --git a/test/integration/basic/pages/forwardRef-component.js b/test/integration/basic/pages/forwardRef-component.js
new file mode 100644
index 00000000..144abe74
--- /dev/null
+++ b/test/integration/basic/pages/forwardRef-component.js
@@ -0,0 +1,7 @@
+import React from 'react'
+
+export default React.forwardRef((props, ref) => (
+
+ This is a component with a forwarded ref
+
+))
diff --git a/test/integration/basic/pages/memo-component.js b/test/integration/basic/pages/memo-component.js
new file mode 100644
index 00000000..6bb12c46
--- /dev/null
+++ b/test/integration/basic/pages/memo-component.js
@@ -0,0 +1,3 @@
+import React from 'react'
+
+export default React.memo((props) => Memo component)
diff --git a/test/integration/basic/test/error-recovery.js b/test/integration/basic/test/error-recovery.js
index c080541e..57b50a9d 100644
--- a/test/integration/basic/test/error-recovery.js
+++ b/test/integration/basic/test/error-recovery.js
@@ -211,7 +211,7 @@ export default (context) => {
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
- aboutPage.replace('export default', 'export default "not-a-page"\nexport const fn = ')
+ aboutPage.replace('export default', 'export default {};\nexport const fn =')
await check(
() => getBrowserBodyText(browser),
@@ -250,7 +250,7 @@ export default (context) => {
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
- aboutPage.replace('export default', 'export default () => /search/ \nexport const fn = ')
+ aboutPage.replace('export default', 'export default () => /search/;\nexport const fn =')
await check(
() => getBrowserBodyText(browser),
diff --git a/test/integration/basic/test/rendering.js b/test/integration/basic/test/rendering.js
index a2381afc..e370ef49 100644
--- a/test/integration/basic/test/rendering.js
+++ b/test/integration/basic/test/rendering.js
@@ -22,6 +22,16 @@ export default function ({ app }, suiteName, render, fetch) {
expect(html.includes('My component!')).toBeTruthy()
})
+ test('renders when component is a forwardRef instance', async () => {
+ const html = await render('/forwardRef-component')
+ expect(html.includes('This is a component with a forwarded ref')).toBeTruthy()
+ })
+
+ test('renders when component is a memo instance', async () => {
+ const html = await render('/memo-component')
+ expect(html.includes('Memo component')).toBeTruthy()
+ })
+
// default-head contains an empty