Skip to content
Merged
21 changes: 21 additions & 0 deletions .changeset/shaggy-keys-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@biomejs/biome": patch
---

Added the rule [`noNextAsyncClientComponent`](https://biomejs.dev/linter/rules/no-next-async-client-component).

This rule prevents the use of async functions for client components in Next.js applications. Client components marked with "use client" directive should not be async as this can cause hydration mismatches, break component rendering lifecycle, and lead to unexpected behavior with React's concurrent features.

```jsx
"use client";

// Invalid - async client component
export default async function MyComponent() {
return <div>Hello</div>;
}

// Valid - synchronous client component
export default function MyComponent() {
return <div>Hello</div>;
}
```
12 changes: 12 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

235 changes: 128 additions & 107 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ define_categories! {
"lint/correctness/useValidTypeof": "https://biomejs.dev/linter/rules/use-valid-typeof",
"lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield",
"lint/nursery/colorNoInvalidHex": "https://biomejs.dev/linter/rules/color-no-invalid-hex",
"lint/nursery/noNextAsyncClientComponent": "https://biomejs.dev/linter/rules/no-next-async-client-component",
"lint/nursery/noAwaitInLoop": "https://biomejs.dev/linter/rules/no-await-in-loop",
"lint/nursery/noBitwiseOperators": "https://biomejs.dev/linter/rules/no-bitwise-operators",
"lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod no_import_cycles;
pub mod no_magic_numbers;
pub mod no_misused_promises;
pub mod no_nested_component_definitions;
pub mod no_next_async_client_component;
pub mod no_non_null_asserted_optional_chain;
pub mod no_noninteractive_element_interactions;
pub mod no_process_global;
Expand Down Expand Up @@ -60,4 +61,4 @@ pub mod use_sorted_classes;
pub mod use_symbol_description;
pub mod use_unified_type_signature;
pub mod use_unique_element_ids;
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_excessive_lines_per_function :: NoExcessiveLinesPerFunction , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_implicit_coercion :: NoImplicitCoercion , self :: no_import_cycles :: NoImportCycles , self :: no_magic_numbers :: NoMagicNumbers , self :: no_misused_promises :: NoMisusedPromises , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_react_prop_assign :: NoReactPropAssign , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unassigned_variables :: NoUnassignedVariables , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_image_size :: UseImageSize , self :: use_index_of :: UseIndexOf , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_json_import_attribute :: UseJsonImportAttribute , self :: use_max_params :: UseMaxParams , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_readonly_class_properties :: UseReadonlyClassProperties , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unified_type_signature :: UseUnifiedTypeSignature , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_excessive_lines_per_function :: NoExcessiveLinesPerFunction , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_implicit_coercion :: NoImplicitCoercion , self :: no_import_cycles :: NoImportCycles , self :: no_magic_numbers :: NoMagicNumbers , self :: no_misused_promises :: NoMisusedPromises , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_react_prop_assign :: NoReactPropAssign , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unassigned_variables :: NoUnassignedVariables , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_image_size :: UseImageSize , self :: use_index_of :: UseIndexOf , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_json_import_attribute :: UseJsonImportAttribute , self :: use_max_params :: UseMaxParams , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_readonly_class_properties :: UseReadonlyClassProperties , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unified_type_signature :: UseUnifiedTypeSignature , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use crate::react::components::{
AnyPotentialReactComponentDeclaration, ReactComponentInfo, ReactComponentKind,
};
use biome_analyze::{
Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_js_syntax::{AnyJsExpression, AnyJsRoot};
use biome_rowan::{AstNode, TokenText};
use biome_rule_options::no_next_async_client_component::NoNextAsyncClientComponentOptions;

declare_lint_rule! {
/// Prevent client components from being async functions.
///
/// This rule prevents the use of async functions for client components in Next.js applications.
/// Client components marked with "use client" directive should not be async as this can cause
/// hydration mismatches, break component rendering lifecycle, and lead to unexpected behavior
/// with React's concurrent features.
///
/// ## Examples
///
/// ### Invalid
///
/// ```jsx,expect_diagnostic
/// "use client";
///
/// export default async function MyComponent() {
/// return <div>Hello</div>;
/// }
/// ```
///
/// ### Valid
///
/// ```jsx
/// "use client";
///
/// export default function MyComponent() {
/// return <div>Hello</div>;
/// }
/// ```
///
/// ```jsx
/// // No "use client" directive - server component can be async
/// export default async function ServerComponent() {
/// const data = await fetch('/api/data');
/// return <div>{data}</div>;
/// }
/// ```
///
pub NoNextAsyncClientComponent {
version: "next",
name: "noNextAsyncClientComponent",
language: "js",
sources: &[RuleSource::EslintNext("no-async-client-component").same()],
recommended: false,
severity: Severity::Warning,
domains: &[RuleDomain::Next],
}
}

impl Rule for NoNextAsyncClientComponent {
type Query = Ast<AnyPotentialReactComponentDeclaration>;
type State = Option<TokenText>;
type Signals = Option<Self::State>;
type Options = NoNextAsyncClientComponentOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let declaration = ctx.query();
let component = ReactComponentInfo::from_declaration(declaration.syntax())?;

// Only check function components
let ReactComponentKind::Function(_) = component.kind else {
return None;
};

// Check if we're in a module with "use client" directive
let root = ctx.root();
let has_use_client = match root {
AnyJsRoot::JsModule(module) => has_use_client_directive(module.directives()),
AnyJsRoot::JsScript(script) => has_use_client_directive(script.directives()),
_ => false,
};

if !has_use_client {
return None;
}

// Check if the component function is async
let is_async = match declaration {
AnyPotentialReactComponentDeclaration::JsFunctionDeclaration(func) => {
func.async_token().is_some()
}
AnyPotentialReactComponentDeclaration::JsFunctionExportDefaultDeclaration(func) => {
func.async_token().is_some()
}
AnyPotentialReactComponentDeclaration::JsVariableDeclarator(declarator) => declarator
.initializer()
.and_then(|init| init.expression().ok())
.and_then(|expr| match expr {
AnyJsExpression::JsArrowFunctionExpression(arrow) => arrow.async_token(),
AnyJsExpression::JsFunctionExpression(func) => func.async_token(),
_ => None,
})
.is_some(),
AnyPotentialReactComponentDeclaration::JsAssignmentExpression(assignment) => assignment
.right()
.ok()
.and_then(|expr| match expr {
AnyJsExpression::JsArrowFunctionExpression(arrow) => arrow.async_token(),
AnyJsExpression::JsFunctionExpression(func) => func.async_token(),
_ => None,
})
.is_some(),
AnyPotentialReactComponentDeclaration::JsExportDefaultExpressionClause(export) => {
export
.expression()
.ok()
.and_then(|expr| match expr {
AnyJsExpression::JsArrowFunctionExpression(arrow) => arrow.async_token(),
AnyJsExpression::JsFunctionExpression(func) => func.async_token(),
_ => None,
})
.is_some()
}
AnyPotentialReactComponentDeclaration::JsMethodObjectMember(method) => {
method.async_token().is_some()
}
AnyPotentialReactComponentDeclaration::JsPropertyObjectMember(prop) => prop
.value()
.ok()
.and_then(|expr| match expr {
AnyJsExpression::JsArrowFunctionExpression(arrow) => arrow.async_token(),
AnyJsExpression::JsFunctionExpression(func) => func.async_token(),
_ => None,
})
.is_some(),
AnyPotentialReactComponentDeclaration::JsMethodClassMember(method) => {
method.async_token().is_some()
}
_ => false,
};

if !is_async {
return None;
}

let component_name = component
.name
.or(component.name_hint)
.map(|token| token.token_text_trimmed());

Some(component_name)
}

fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let declaration = ctx.query();

Some(match state {
Some(component_name) => {
let name_text = component_name.text();
RuleDiagnostic::new(
rule_category!(),
declaration.range(),
markup! {
"The component "<Emphasis>{name_text}</Emphasis>" is an async client component, which is not allowed."
},
)
}
None => {
RuleDiagnostic::new(
rule_category!(),
declaration.range(),
markup! {
"Async client component are not allowed."
},
)
}
}
.note(markup! {
"Client components with \"use client\" directive should not be async functions as this can cause hydration mismatches and break React's rendering lifecycle."
})
.note(markup! {
"Consider using useEffect for async operations inside the component, or remove the \"use client\" directive if this should be a server component."
}))
}
}

fn has_use_client_directive(
directives: impl IntoIterator<Item = biome_js_syntax::JsDirective>,
) -> bool {
directives.into_iter().any(|directive| {
directive
.inner_string_text()
.is_ok_and(|text| text.text() == "use client")
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

export default async function MyComponent() {
return <div>Hello</div>;
}

async function MyComponent2() {
return <div>Hello</div>;
}

const MyComponent3 = async () => {
return <div>Hello</div>;
};

const MyComponent4 = async function() {
return <div>Hello</div>;
};

let MyComponent5;
MyComponent5 = async () => {
return <div>Hello</div>;
};

let MyComponent6;
MyComponent6 = async function() {
return <div>Hello</div>;
};

const components = {
async MyComponent() {
return <div>Hello</div>;
}
};

const components2 = {
MyComponent: async () => {
return <div>Hello</div>;
}
};

const components3 = {
MyComponent: async function() {
return <div>Hello</div>;
}
};

class ComponentClass {
async MyComponent() {
return <div>Hello</div>;
}
}
Loading
Loading