Skip to content

Commit

Permalink
Add helpful error for createContext used in Server Components (#43747)
Browse files Browse the repository at this point in the history
Format runtime server errors similarly to how build errors [are
formatted](https://github.com/vercel/next.js/blob/canary/packages/next/client/dev/error-overlay/format-webpack-messages.js).
Add helpful message when createContext is used in Server Components.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
hanneslund committed Dec 12, 2022
1 parent af0ac94 commit 50caf8b
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 0 deletions.
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()
})
})
})

0 comments on commit 50caf8b

Please sign in to comment.