mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(linter/react): ignore impure calls in async components in react/purity
Original PR: #27986 Closes #27974
This commit is contained in:
1 parent
419c1b0fd0
commit
61fe2af7f2
4 files changed
+165
No files matched your search
@@ -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 <button onClick={() => setRand(Math.random())}>{rand}</button>;
|
||||
/// }
|
||||
///
|
||||
/// async function Posts({ posts }) {
|
||||
/// return <ul>{posts.map(post => <li key={post.id}>{Date.now()}</li>)}</ul>;
|
||||
/// }
|
||||
/// ```
|
||||
Purity,
|
||||
react,
|
||||
|
||||
@@ -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) => <p key={p.id}>{Date.now() - p.createdAt < 86_400_000 ? "new" : ""}</p>);
|
||||
}
|
||||
|
||||
async function AsyncDeclaration() {
|
||||
const date = Date.now();
|
||||
const now = new Date();
|
||||
const rand = Math.random();
|
||||
return <Foo date={date} now={now} rand={rand} />;
|
||||
}
|
||||
|
||||
const AsyncArrow = async () => {
|
||||
return <p>{Date.now()}</p>;
|
||||
};
|
||||
|
||||
const AsyncFunctionExpression = async function () {
|
||||
return <p>{new Date().getTime()}</p>;
|
||||
};
|
||||
|
||||
async function AsyncMapCallback({ posts }) {
|
||||
return (
|
||||
<ul>
|
||||
{posts.map((p) => (
|
||||
<li key={p.id}>{Date.now() - p.createdAt}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncHelperCallback({ posts }) {
|
||||
const label = (post) => (Date.now() - post.createdAt < 86400000 ? "new" : "");
|
||||
return (
|
||||
<ul>
|
||||
{posts.map((p) => (
|
||||
<li key={p.id}>{label(p)}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncNestedComponent() {
|
||||
function Badge() {
|
||||
return <span>{Date.now()}</span>;
|
||||
}
|
||||
return <Badge />;
|
||||
}
|
||||
|
||||
// Non-async components are still reported, including after an async component.
|
||||
function ClientAfterServer() {
|
||||
return <p>{Date.now()}</p>;
|
||||
}
|
||||
|
||||
const ClientArrow = () => {
|
||||
return <p>{Math.random()}</p>;
|
||||
};
|
||||
|
||||
// Nested callbacks of a non-async component still run on every render.
|
||||
function ClientMapCallback({ posts }) {
|
||||
return (
|
||||
<ul>
|
||||
{posts.map((p) => (
|
||||
<li key={p.id}>{Date.now()}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// Hooks stay checked, including async hooks.
|
||||
function useNow() {
|
||||
return <span>{Date.now()}</span>;
|
||||
}
|
||||
|
||||
async function useAsyncNow() {
|
||||
return <span>{new Date().getTime()}</span>;
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) => <p key={p.id}>{Date.now() - p.createdAt < 864e5 ? "new" : ""}</p>);
|
||||
}
|
||||
async function AsyncDeclaration() {
|
||||
const date = Date.now();
|
||||
const now = new Date();
|
||||
const rand = Math.random();
|
||||
return <Foo date={date} now={now} rand={rand} />;
|
||||
}
|
||||
const AsyncArrow = async () => {
|
||||
return <p>{Date.now()}</p>;
|
||||
};
|
||||
const AsyncFunctionExpression = async function() {
|
||||
return <p>{new Date().getTime()}</p>;
|
||||
};
|
||||
async function AsyncMapCallback({ posts }) {
|
||||
return <ul>
|
||||
{posts.map((p) => <li key={p.id}>{Date.now() - p.createdAt}</li>)}
|
||||
</ul>;
|
||||
}
|
||||
async function AsyncHelperCallback({ posts }) {
|
||||
const label = (post) => Date.now() - post.createdAt < 864e5 ? "new" : "";
|
||||
return <ul>
|
||||
{posts.map((p) => <li key={p.id}>{label(p)}</li>)}
|
||||
</ul>;
|
||||
}
|
||||
async function AsyncNestedComponent() {
|
||||
function Badge() {
|
||||
return <span>{Date.now()}</span>;
|
||||
}
|
||||
return <Badge />;
|
||||
}
|
||||
// Non-async components are still reported, including after an async component.
|
||||
function ClientAfterServer() {
|
||||
return <p>{Date.now()}</p>;
|
||||
}
|
||||
const ClientArrow = () => {
|
||||
return <p>{Math.random()}</p>;
|
||||
};
|
||||
// Nested callbacks of a non-async component still run on every render.
|
||||
function ClientMapCallback({ posts }) {
|
||||
return <ul>
|
||||
{posts.map((p) => <li key={p.id}>{Date.now()}</li>)}
|
||||
</ul>;
|
||||
}
|
||||
// Hooks stay checked, including async hooks.
|
||||
function useNow() {
|
||||
return <span>{Date.now()}</span>;
|
||||
}
|
||||
async function useAsyncNow() {
|
||||
return <span>{new Date().getTime()}</span>;
|
||||
}
|
||||
Reference in new issue
Block a user