-
Notifications
You must be signed in to change notification settings - Fork 27.2k
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
feat: Introduce eslint rule for async client components #51547
Merged
Merged
Changes from 6 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
39596b6
feat: Add lint rule for async client components
tyhopp 835eb33
test: Add test cases for new eslint rule
tyhopp 4ef91f5
chore: Adjust error message to match others
tyhopp 04bffc7
docs: Add new doc error page
tyhopp d97f519
Merge branch 'canary' into feat-eslint-rsc-rule
shuding 6ff7ab5
Merge branch 'canary' into feat-eslint-rsc-rule
delbaoliveira af3c38b
feat: Add capitalized identifier condition
tyhopp 4201927
test: Identifier capitalization condition
tyhopp 3b82c26
refactor: Make eslint rule a warning
tyhopp cd9c9f4
Merge branch 'canary' into feat-eslint-rsc-rule
shuding d60a9ae
Merge branch 'canary' into feat-eslint-rsc-rule
shuding 1477a52
Merge branch 'canary' into feat-eslint-rsc-rule
shuding 1cfc1fc
Merge branch 'canary' into feat-eslint-rsc-rule
kodiakhq[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# No async client component | ||
|
||
> Client components cannot be async functions. | ||
|
||
#### Why This Error Occurred | ||
|
||
As per the [React Server Component RFC on promise support](https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md), [client components cannot be async functions](https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md#why-cant-client-components-be-async-functions). | ||
|
||
#### Possible Ways to Fix It | ||
|
||
1. Remove the `async` keyword from the client component function declaration, or | ||
2. Convert the client component to a server component |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
packages/eslint-plugin-next/src/rules/no-async-client-component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { defineRule } from '../utils/define-rule' | ||
|
||
const url = 'https://nextjs.org/docs/messages/no-async-client-component' | ||
const description = 'Prevent client components from being async functions.' | ||
const message = `${description} See: ${url}` | ||
|
||
export = defineRule({ | ||
meta: { | ||
docs: { | ||
description, | ||
recommended: true, | ||
url, | ||
}, | ||
type: 'problem', | ||
schema: [], | ||
}, | ||
|
||
create(context) { | ||
return { | ||
Program(node) { | ||
let isClientComponent: boolean = false | ||
|
||
for (const block of node.body) { | ||
if ( | ||
block.type === 'ExpressionStatement' && | ||
block.expression.type === 'Literal' && | ||
block.expression.value === 'use client' | ||
) { | ||
isClientComponent = true | ||
} | ||
|
||
if (block.type === 'ExportDefaultDeclaration' && isClientComponent) { | ||
// export default async function MyComponent() {...} | ||
if ( | ||
block.declaration.type === 'FunctionDeclaration' && | ||
block.declaration.async | ||
) { | ||
context.report({ | ||
node: block, | ||
message, | ||
}) | ||
} | ||
|
||
// async function MyComponent() {...}; export default MyComponent; | ||
if (block.declaration.type === 'Identifier') { | ||
const functionName = block.declaration.name | ||
const functionDeclaration = node.body.find( | ||
(localBlock) => | ||
localBlock.type === 'FunctionDeclaration' && | ||
localBlock.id.name === functionName | ||
) | ||
|
||
if ( | ||
functionDeclaration.type === 'FunctionDeclaration' && | ||
functionDeclaration.async | ||
) { | ||
context.report({ | ||
node: functionDeclaration, | ||
message, | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
} | ||
}, | ||
}) |
72 changes: 72 additions & 0 deletions
72
test/unit/eslint-plugin-next/no-async-client-component.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import rule from '@next/eslint-plugin-next/dist/rules/no-async-client-component' | ||
import { RuleTester } from 'eslint' | ||
;(RuleTester as any).setDefaultConfig({ | ||
parserOptions: { | ||
ecmaVersion: 2018, | ||
sourceType: 'module', | ||
ecmaFeatures: { | ||
modules: true, | ||
jsx: true, | ||
}, | ||
}, | ||
}) | ||
const ruleTester = new RuleTester() | ||
|
||
const message = | ||
'Prevent client components from being async functions. See: https://nextjs.org/docs/messages/no-async-client-component' | ||
|
||
ruleTester.run('no-async-client-component single line', rule, { | ||
valid: [ | ||
` | ||
export default async function MyComponent() { | ||
return <></> | ||
} | ||
`, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
"use client" | ||
|
||
export default async function MyComponent() { | ||
return <></> | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
message, | ||
}, | ||
], | ||
}, | ||
], | ||
}) | ||
|
||
ruleTester.run('no-async-client-component multiple line', rule, { | ||
valid: [ | ||
` | ||
async function MyComponent() { | ||
return <></> | ||
} | ||
|
||
export default MyComponent | ||
`, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
"use client" | ||
|
||
async function MyComponent() { | ||
return <></> | ||
} | ||
|
||
export default MyComponent | ||
`, | ||
errors: [ | ||
{ | ||
message, | ||
}, | ||
], | ||
}, | ||
], | ||
}) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tricky thing about this being a linter rule is that, even if a file doesn't have
"use client"
on top, it can still be a Client Component (e.g. it can be imported by a file with"use client"
). And also, a file with"use client"
can still export non-component functions that are async. Not strongly against adding such a linter rule, but we can probably make it a warning instead of an error, and ensure that the exported function's name start with a capitalized character.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the review @shuding! I pushed the changes you suggested.
Agree this as a linter rule isn't a complete solution. In addition to the case you mentioned it also does not handle anonymous default exports or named exports. It is low-hanging fruit that should help prevent the page crashes people are reporting in #50898 and #50382, at least for ESLint users using Next's recommended config.
Linter rule aside, is it possible to throw an error at runtime for this scenario? That way it it avoids the page crash and provides some recourse to developers in the error message. As it stands right now there is no indication why the page crash happens
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Yeah it sounds good to have this shipped first, at the same time we'll check how this can be improved over time.
Unfortunately we can't add this as a runtime check. We can't abort synchronous operations in that case.