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 new file mode 100644 index 000000000000000..477a837741d308d --- /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 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') + 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 3b793ba777e35bc..336686fbe51f339 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 @@ -1027,6 +1028,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..5c76475e0fc1da1 --- /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 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. + +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 React.createContext is called in external package 1`] = ` +" 1 of 1 unhandled error +Server Error + +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 createContext is called in external package 1`] = ` +" 1 of 1 unhandled error +Server Error + +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" +`; 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..47246440498c07c --- /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', () => { + it('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() + }) + + it('should show error when React.createContext is called in external package', 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() + }) + + it('should show error when createContext is called in external package', 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() + }) + }) +})