From c05bcbbce5326be1351c9d3b62eedd41f350e0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Tue, 6 Dec 2022 07:22:32 +0100 Subject: [PATCH 1/6] Add helpful error for createContext in Server Components --- packages/next/lib/format-server-error.ts | 12 ++ packages/next/server/dev/next-dev-server.ts | 2 + .../server-components.test.ts.snap | 42 +++++ .../fixtures/default-template/index.js | 8 +- .../acceptance-app/server-components.test.ts | 160 ++++++++++++++++++ 5 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 packages/next/lib/format-server-error.ts create mode 100644 test/development/acceptance-app/__snapshots__/server-components.test.ts.snap create mode 100644 test/development/acceptance-app/server-components.test.ts diff --git a/packages/next/lib/format-server-error.ts b/packages/next/lib/format-server-error.ts new file mode 100644 index 000000000000000..7b1e28dd0120419 --- /dev/null +++ b/packages/next/lib/format-server-error.ts @@ -0,0 +1,12 @@ +export function formatServerError(error: Error): void { + if (error.message.includes('createContext is not a function')) { + const message = + 'createContext only works in Client Components. Add "use client" at the top of your file to use createContext. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context' + error.message = message + if (error.stack) { + const lines = error.stack.split('\n') + lines[0] = message + error.stack = lines.join('\n') + } + } +} diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index c4dae0ce636173f..b32cc6b753a9b7b 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -76,6 +76,7 @@ import { } from '../../build/utils' import { getDefineEnv } from '../../build/webpack-config' import loadJsConfig from '../../build/load-jsconfig' +import { formatServerError } from '../../lib/format-server-error' // Load ReactDevOverlay only when needed let ReactDevOverlayImpl: FunctionComponent @@ -1025,6 +1026,7 @@ export default class DevServer extends Server { return await super.run(req, res, parsedUrl) } catch (error) { const err = getProperError(error) + formatServerError(err) this.logErrorWithOriginalStack(err).catch(() => {}) if (!res.sent) { res.statusCode = 500 diff --git a/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap b/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap new file mode 100644 index 000000000000000..2b9f264942994b7 --- /dev/null +++ b/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Error Overlay for server components createContext called in Server Component should show error when React.createContext is called 1`] = ` +" 1 of 1 unhandled error +Server Error + +TypeError: createContext only works in Client Components. Add \\"use client\\" at the top of your file to use createContext. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context + +This error happened while generating the page. Any console logs will be displayed in the terminal window. + +app/page.js (3:24) @ React + + 1 | + 2 | import React from 'react' +> 3 | const Context = React.createContext() + | ^ + 4 | export default function Page() { + 5 | return ( + 6 | <>" +`; + +exports[`Error Overlay for server components createContext called in Server Component should show error when external package React.createContext is called 1`] = ` +" 1 of 1 unhandled error +Server Error + +TypeError: createContext only works in Client Components. Add \\"use client\\" at the top of your file to use createContext. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context + +This error happened while generating the page. Any console logs will be displayed in the terminal window. + +null" +`; + +exports[`Error Overlay for server components createContext called in Server Component should show error when external package createContext is called 1`] = ` +" 1 of 1 unhandled error +Server Error + +TypeError: createContext only works in Client Components. Add \\"use client\\" at the top of your file to use createContext. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context + +This error happened while generating the page. Any console logs will be displayed in the terminal window. + +null" +`; diff --git a/test/development/acceptance-app/fixtures/default-template/index.js b/test/development/acceptance-app/fixtures/default-template/index.js index 31fd86d55937d47..370b79ba813f592 100644 --- a/test/development/acceptance-app/fixtures/default-template/index.js +++ b/test/development/acceptance-app/fixtures/default-template/index.js @@ -1 +1,7 @@ -export default () => 'new sandbox' +export default () => { + if (typeof window !== 'undefined') { + throw new Error('test') + } + + return null +} diff --git a/test/development/acceptance-app/server-components.test.ts b/test/development/acceptance-app/server-components.test.ts new file mode 100644 index 000000000000000..2045b208406042f --- /dev/null +++ b/test/development/acceptance-app/server-components.test.ts @@ -0,0 +1,160 @@ +/* eslint-env jest */ +import { sandbox } from './helpers' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import path from 'path' + +describe('Error Overlay for server components', () => { + if (process.env.NEXT_TEST_REACT_VERSION === '^17') { + it('should skip for react v17', () => {}) + return + } + + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, + }) + }) + afterAll(() => next.destroy()) + + describe('createContext called in Server Component', () => { + test('should show error when React.createContext is called', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + ` + import React from 'react' + const Context = React.createContext() + export default function Page() { + return ( + <> + +

Page

+
+ + ) + }`, + ], + ]) + ) + + // TODO-APP: currently requires a full reload because moving from a client component to a server component isn't causing a Fast Refresh yet. + await browser.refresh() + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource(true)).toMatchSnapshot() + expect(next.cliOutput).toContain( + 'createContext only works in Client Components' + ) + + await cleanup() + }) + + test('should show error when external package React.createContext is called', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/my-package/index.js', + ` + const React = require('react') + module.exports = React.createContext() + `, + ], + [ + 'node_modules/my-package/package.json', + ` + { + "name": "my-package", + "version": "0.0.1" + } + `, + ], + [ + 'app/page.js', + ` + import Context from 'my-package' + export default function Page() { + return ( + <> + +

Page

+
+ + ) + }`, + ], + ]) + ) + + // TODO-APP: currently requires a full reload because moving from a client component to a server component isn't causing a Fast Refresh yet. + await browser.refresh() + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource(true)).toMatchSnapshot() + expect(next.cliOutput).toContain( + 'createContext only works in Client Components' + ) + + await cleanup() + }) + + test('should show error when external package createContext is called', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/my-package/index.js', + ` + const { createContext } = require('react') + module.exports = createContext() + `, + ], + [ + 'node_modules/my-package/package.json', + ` + { + "name": "my-package", + "version": "0.0.1" + } + `, + ], + [ + 'app/page.js', + ` + import Context from 'my-package' + export default function Page() { + return ( + <> + +

Page

+
+ + ) + }`, + ], + ]) + ) + + // TODO-APP: currently requires a full reload because moving from a client component to a server component isn't causing a Fast Refresh yet. + await browser.refresh() + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource(true)).toMatchSnapshot() + expect(next.cliOutput).toContain( + 'createContext only works in Client Components' + ) + + await cleanup() + }) + }) +}) From 055787bb89a6f3a11b318fa6ed081e81d742e004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Tue, 6 Dec 2022 12:31:58 +0100 Subject: [PATCH 2/6] Fix template file --- .../acceptance-app/fixtures/default-template/index.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/development/acceptance-app/fixtures/default-template/index.js b/test/development/acceptance-app/fixtures/default-template/index.js index 370b79ba813f592..31fd86d55937d47 100644 --- a/test/development/acceptance-app/fixtures/default-template/index.js +++ b/test/development/acceptance-app/fixtures/default-template/index.js @@ -1,7 +1 @@ -export default () => { - if (typeof window !== 'undefined') { - throw new Error('test') - } - - return null -} +export default () => 'new sandbox' From 129f00a6e703ab14926dbd3846714037298e8469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Tue, 6 Dec 2022 12:33:23 +0100 Subject: [PATCH 3/6] Fix test names --- test/development/acceptance-app/server-components.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/development/acceptance-app/server-components.test.ts b/test/development/acceptance-app/server-components.test.ts index 2045b208406042f..47246440498c07c 100644 --- a/test/development/acceptance-app/server-components.test.ts +++ b/test/development/acceptance-app/server-components.test.ts @@ -25,7 +25,7 @@ describe('Error Overlay for server components', () => { afterAll(() => next.destroy()) describe('createContext called in Server Component', () => { - test('should show error when React.createContext is called', async () => { + it('should show error when React.createContext is called', async () => { const { session, browser, cleanup } = await sandbox( next, new Map([ @@ -59,7 +59,7 @@ describe('Error Overlay for server components', () => { await cleanup() }) - test('should show error when external package React.createContext is called', async () => { + it('should show error when React.createContext is called in external package', async () => { const { session, browser, cleanup } = await sandbox( next, new Map([ @@ -108,7 +108,7 @@ describe('Error Overlay for server components', () => { await cleanup() }) - test('should show error when external package createContext is called', async () => { + it('should show error when createContext is called in external package', async () => { const { session, browser, cleanup } = await sandbox( next, new Map([ From f8cf75730d077946c524d6fde1d63fc5563eeff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Tue, 6 Dec 2022 13:35:45 +0100 Subject: [PATCH 4/6] Update error message --- packages/next/lib/format-server-error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/lib/format-server-error.ts b/packages/next/lib/format-server-error.ts index 7b1e28dd0120419..73ffafbc30f2734 100644 --- a/packages/next/lib/format-server-error.ts +++ b/packages/next/lib/format-server-error.ts @@ -1,7 +1,7 @@ export function formatServerError(error: Error): void { if (error.message.includes('createContext is not a function')) { const message = - 'createContext only works in Client Components. Add "use client" at the top of your file to use createContext. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context' + 'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context' error.message = message if (error.stack) { const lines = error.stack.split('\n') From 471df8e45438988d4ffd91f9dd57e4e2e89e4408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Tue, 6 Dec 2022 13:37:30 +0100 Subject: [PATCH 5/6] Update snapshots --- .../__snapshots__/server-components.test.ts.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap b/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap index 2b9f264942994b7..5c76475e0fc1da1 100644 --- a/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap +++ b/test/development/acceptance-app/__snapshots__/server-components.test.ts.snap @@ -4,7 +4,7 @@ exports[`Error Overlay for server components createContext called in Server Comp " 1 of 1 unhandled error Server Error -TypeError: createContext only works in Client Components. Add \\"use client\\" at the top of your file to use createContext. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context +TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context This error happened while generating the page. Any console logs will be displayed in the terminal window. @@ -19,22 +19,22 @@ app/page.js (3:24) @ React 6 | <>" `; -exports[`Error Overlay for server components createContext called in Server Component should show error when external package React.createContext is called 1`] = ` +exports[`Error Overlay for server components createContext called in Server Component should show error when React.createContext is called in external package 1`] = ` " 1 of 1 unhandled error Server Error -TypeError: createContext only works in Client Components. Add \\"use client\\" at the top of your file to use createContext. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context +TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context This error happened while generating the page. Any console logs will be displayed in the terminal window. null" `; -exports[`Error Overlay for server components createContext called in Server Component should show error when external package createContext is called 1`] = ` +exports[`Error Overlay for server components createContext called in Server Component should show error when createContext is called in external package 1`] = ` " 1 of 1 unhandled error Server Error -TypeError: createContext only works in Client Components. Add \\"use client\\" at the top of your file to use createContext. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context +TypeError: createContext only works in Client Components. Add the \\"use client\\" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context This error happened while generating the page. Any console logs will be displayed in the terminal window. From 856b9459f8928fcf1f2837b1ed046afb55f28fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Wed, 7 Dec 2022 15:28:02 +0100 Subject: [PATCH 6/6] Use error instead of link --- errors/context-in-server-component.md | 30 ++++++++++++++++++++++++ errors/manifest.json | 4 ++++ packages/next/lib/format-server-error.ts | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 errors/context-in-server-component.md diff --git a/errors/context-in-server-component.md b/errors/context-in-server-component.md new file mode 100644 index 000000000000000..1dcbfd8a2f7ce39 --- /dev/null +++ b/errors/context-in-server-component.md @@ -0,0 +1,30 @@ +# createContext in a Server Component + +#### Why This Error Occurred + +You are using `createContext` in a Server Component but it only works in Client Components. + +#### Possible Ways to Fix It + +Mark the component using `createContext` as a Client Component by adding `'use client'` at the top of the file. + +##### Before + +```jsx +import { createContext } from 'react' + +const Context = createContext() +``` + +##### After + +```jsx +'use client' +import { createContext } from 'react' + +const Context = createContext() +``` + +### Useful Links + +[Server and Client Components](https://beta.nextjs.org/docs/rendering/server-and-client-components#context) diff --git a/errors/manifest.json b/errors/manifest.json index f2ed0ae1656e749..7273d829894c5ec 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -773,6 +773,10 @@ { "title": "fast-refresh-reload", "path": "/errors/fast-refresh-reload.md" + }, + { + "title": "context-in-server-component", + "path": "/errors/context-in-server-component.md" } ] } diff --git a/packages/next/lib/format-server-error.ts b/packages/next/lib/format-server-error.ts index 73ffafbc30f2734..477a837741d308d 100644 --- a/packages/next/lib/format-server-error.ts +++ b/packages/next/lib/format-server-error.ts @@ -1,7 +1,7 @@ export function formatServerError(error: Error): void { if (error.message.includes('createContext is not a function')) { const message = - 'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://beta.nextjs.org/docs/rendering/server-and-client-components#context' + 'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component' error.message = message if (error.stack) { const lines = error.stack.split('\n')