diff --git a/crates/oxc_linter/src/rules/react/purity.rs b/crates/oxc_linter/src/rules/react/purity.rs
index 74cd6ec290..63acd4cb9f 100644
--- a/crates/oxc_linter/src/rules/react/purity.rs
+++ b/crates/oxc_linter/src/rules/react/purity.rs
@@ -16,6 +16,10 @@ declare_react_compiler_lint!(
/// not call known-impure functions such as `Math.random()`, `Date.now()`,
/// or `performance.now()` during render.
///
+ /// Async function components are exempt, including impure calls in nested
+ /// callbacks. An async component is a Server Component: it renders once per
+ /// request and does not re-render. Async hooks are still checked.
+ ///
upstream = "purity",
///
/// ### Why is this bad?
@@ -40,6 +44,10 @@ declare_react_compiler_lint!(
/// const [rand, setRand] = useState(0);
/// return setRand(Math.random())}>{rand} ;
/// }
+ ///
+ /// async function Posts({ posts }) {
+ /// return
{posts.map(post => {Date.now()} )} ;
+ /// }
/// ```
Purity,
react,
diff --git a/crates/oxc_react_compiler/fixtures/purity-async-server-component.js b/crates/oxc_react_compiler/fixtures/purity-async-server-component.js
new file mode 100644
index 0000000000..a5e2c9b7ad
--- /dev/null
+++ b/crates/oxc_react_compiler/fixtures/purity-async-server-component.js
@@ -0,0 +1,79 @@
+// @validateNoImpureFunctionsInRender @outputMode:"lint" @compilationMode:"infer" @panicThreshold:"none"
+// Async function components are Server Components: they render once per request and never
+// re-render, so impure calls are not reported in them or in their nested callbacks.
+export default async function Posts() {
+ const posts = await getPosts();
+ return posts.map((p) => {Date.now() - p.createdAt < 86_400_000 ? "new" : ""}
);
+}
+
+async function AsyncDeclaration() {
+ const date = Date.now();
+ const now = new Date();
+ const rand = Math.random();
+ return ;
+}
+
+const AsyncArrow = async () => {
+ return {Date.now()}
;
+};
+
+const AsyncFunctionExpression = async function () {
+ return {new Date().getTime()}
;
+};
+
+async function AsyncMapCallback({ posts }) {
+ return (
+
+ {posts.map((p) => (
+ {Date.now() - p.createdAt}
+ ))}
+
+ );
+}
+
+async function AsyncHelperCallback({ posts }) {
+ const label = (post) => (Date.now() - post.createdAt < 86400000 ? "new" : "");
+ return (
+
+ {posts.map((p) => (
+ {label(p)}
+ ))}
+
+ );
+}
+
+async function AsyncNestedComponent() {
+ function Badge() {
+ return {Date.now()} ;
+ }
+ return ;
+}
+
+// Non-async components are still reported, including after an async component.
+function ClientAfterServer() {
+ return {Date.now()}
;
+}
+
+const ClientArrow = () => {
+ return {Math.random()}
;
+};
+
+// Nested callbacks of a non-async component still run on every render.
+function ClientMapCallback({ posts }) {
+ return (
+
+ {posts.map((p) => (
+ {Date.now()}
+ ))}
+
+ );
+}
+
+// Hooks stay checked, including async hooks.
+function useNow() {
+ return {Date.now()} ;
+}
+
+async function useAsyncNow() {
+ return {new Date().getTime()} ;
+}
diff --git a/crates/oxc_react_compiler/src/react_compiler/entrypoint/pipeline.rs b/crates/oxc_react_compiler/src/react_compiler/entrypoint/pipeline.rs
index 55a9c5bcd9..2344167db0 100644
--- a/crates/oxc_react_compiler/src/react_compiler/entrypoint/pipeline.rs
+++ b/crates/oxc_react_compiler/src/react_compiler/entrypoint/pipeline.rs
@@ -170,6 +170,16 @@ pub fn compile_fn<'a, const EMIT: bool>(
optimize_props_method_calls(&mut hir, &env);
+ // Async function components are Server Components: they render once per
+ // request and never re-render, so `Date.now()` / `Math.random()` are not a
+ // purity hazard. Nested functions are analysed with this same config, which
+ // covers callbacks that run during that render. Async hooks are not
+ // components and stay checked. The upstream React Compiler does not
+ // special-case this (facebook/react#34834).
+ if hir.is_async && env.fn_type == ReactFunctionType::Component {
+ env.config.validate_no_impure_functions_in_render = false;
+ }
+
analyse_functions(&mut hir, &mut env, &mut |_inner_func, _inner_env| {})?;
if env.has_invariant_errors() {
diff --git a/crates/oxc_react_compiler/tests/snapshots/purity-async-server-component.snap b/crates/oxc_react_compiler/tests/snapshots/purity-async-server-component.snap
new file mode 100644
index 0000000000..810988ceaf
--- /dev/null
+++ b/crates/oxc_react_compiler/tests/snapshots/purity-async-server-component.snap
@@ -0,0 +1,68 @@
+---
+source: crates/oxc_react_compiler/tests/snapshot.rs
+input_file: crates/oxc_react_compiler/fixtures/purity-async-server-component.js
+---
+Diagnostics:
+
+OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 1460, end: 1470 }, primary: true }], help: Some("`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
+OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 1521, end: 1534 }, primary: true }], help: Some("`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
+OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 1727, end: 1737 }, primary: true }], help: Some("`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
+OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 1854, end: 1864 }, primary: true }], help: Some("`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
+OxcDiagnostic { inner: OxcDiagnosticInner { message: "Cannot call impure function during render", labels: [LabeledSpan { label: Some("Cannot call impure function"), span: Span { start: 1924, end: 1934 }, primary: true }], help: Some("`Date` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component re-renders"), note: Some("React Compiler skipped optimizing this component or hook"), severity: Error, code: OxcCode { scope: Some("react-compiler"), number: Some("Purity") }, url: Some("https://react.dev/reference/eslint-plugin-react-hooks/lints/purity") } }
+
+// @validateNoImpureFunctionsInRender @outputMode:"lint" @compilationMode:"infer" @panicThreshold:"none"
+// Async function components are Server Components: they render once per request and never
+// re-render, so impure calls are not reported in them or in their nested callbacks.
+export default async function Posts() {
+ const posts = await getPosts();
+ return posts.map((p) => {Date.now() - p.createdAt < 864e5 ? "new" : ""}
);
+}
+async function AsyncDeclaration() {
+ const date = Date.now();
+ const now = new Date();
+ const rand = Math.random();
+ return ;
+}
+const AsyncArrow = async () => {
+ return {Date.now()}
;
+};
+const AsyncFunctionExpression = async function() {
+ return {new Date().getTime()}
;
+};
+async function AsyncMapCallback({ posts }) {
+ return
+ {posts.map((p) => {Date.now() - p.createdAt} )}
+ ;
+}
+async function AsyncHelperCallback({ posts }) {
+ const label = (post) => Date.now() - post.createdAt < 864e5 ? "new" : "";
+ return
+ {posts.map((p) => {label(p)} )}
+ ;
+}
+async function AsyncNestedComponent() {
+ function Badge() {
+ return {Date.now()} ;
+ }
+ return ;
+}
+// Non-async components are still reported, including after an async component.
+function ClientAfterServer() {
+ return {Date.now()}
;
+}
+const ClientArrow = () => {
+ return {Math.random()}
;
+};
+// Nested callbacks of a non-async component still run on every render.
+function ClientMapCallback({ posts }) {
+ return
+ {posts.map((p) => {Date.now()} )}
+ ;
+}
+// Hooks stay checked, including async hooks.
+function useNow() {
+ return {Date.now()} ;
+}
+async function useAsyncNow() {
+ return {new Date().getTime()} ;
+}