Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helpful error for createContext used in Server Components #43747

Merged
merged 16 commits into from Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions 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)
4 changes: 4 additions & 0 deletions errors/manifest.json
Expand Up @@ -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"
}
]
}
Expand Down
12 changes: 12 additions & 0 deletions 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')
}
}
}
2 changes: 2 additions & 0 deletions packages/next/server/dev/next-dev-server.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
@@ -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"
`;
160 changes: 160 additions & 0 deletions 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 (
<>
<Context.Provider value="hello">
<h1>Page</h1>
</Context.Provider>
</>
)
}`,
],
])
)

// 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 (
<>
<Context.Provider value="hello">
<h1>Page</h1>
</Context.Provider>
</>
)
}`,
],
])
)

// 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 (
<>
<Context.Provider value="hello">
<h1>Page</h1>
</Context.Provider>
</>
)
}`,
],
])
)

// 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()
})
})
})