Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react-doctor/src/oxlint-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const createOxlintConfig = ({
"react-doctor/rerender-memo-with-default-value": "warn",
"react-doctor/rendering-animate-svg-wrapper": "warn",
"react-doctor/no-inline-prop-on-memo-component": "warn",
"react-doctor/no-inline-object-props": "warn",
"react-doctor/rendering-hydration-no-flicker": "warn",

"react-doctor/no-transition-all": "warn",
Expand Down
2 changes: 2 additions & 0 deletions packages/react-doctor/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
} from "./rules/nextjs.js";
import {
noGlobalCssVariableAnimation,
noInlineObjectProps,
noLargeAnimatedBlur,
noLayoutPropertyAnimation,
noPermanentWillChange,
Expand Down Expand Up @@ -107,6 +108,7 @@ const plugin: RulePlugin = {
"rerender-memo-with-default-value": rerenderMemoWithDefaultValue,
"rendering-animate-svg-wrapper": renderingAnimateSvgWrapper,
"no-inline-prop-on-memo-component": noInlinePropOnMemoComponent,
"no-inline-object-props": noInlineObjectProps,
"rendering-hydration-no-flicker": renderingHydrationNoFlicker,

"no-transition-all": noTransitionAll,
Expand Down
25 changes: 25 additions & 0 deletions packages/react-doctor/src/plugin/rules/performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ const isInlineReference = (node: EsTreeNode): string | null => {
return null;
};

const getInlineLiteralKind = (node: EsTreeNode): "object" | "array" | null => {
if (node.type === "ObjectExpression") return "object";
if (node.type === "ArrayExpression") return "array";
return null;
};

export const noInlineObjectProps: Rule = {
create: (context: RuleContext) => ({
JSXAttribute(node: EsTreeNode) {
if (!node.value || node.value.type !== "JSXExpressionContainer") return;

const inlineLiteralKind = getInlineLiteralKind(node.value.expression);
if (!inlineLiteralKind) return;

const propName =
node.name?.type === "JSXIdentifier" ? node.name.name : "this prop";

context.report({
node: node.value.expression,
message: `Inline ${inlineLiteralKind} literal passed to "${propName}" creates a new reference on every render — extract it to a stable constant`,
});
},
}),
};

export const noInlinePropOnMemoComponent: Rule = {
create: (context: RuleContext) => {
const memoizedComponentNames = new Set<string>();
Expand Down
3 changes: 3 additions & 0 deletions packages/react-doctor/src/utils/run-oxlint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const RULE_CATEGORY_MAP: Record<string, string> = {
"react-doctor/rerender-memo-with-default-value": "Performance",
"react-doctor/rendering-animate-svg-wrapper": "Performance",
"react-doctor/rendering-usetransition-loading": "Performance",
"react-doctor/no-inline-object-props": "Performance",
"react-doctor/rendering-hydration-no-flicker": "Performance",

"react-doctor/no-transition-all": "Performance",
Expand Down Expand Up @@ -124,6 +125,8 @@ const RULE_HELP_MAP: Record<string, string> = {
"Wrap the SVG: `<motion.div animate={...}><svg>...</svg></motion.div>`",
"rendering-usetransition-loading":
"Replace with `const [isPending, startTransition] = useTransition()` — avoids a re-render for the loading state",
"no-inline-object-props":
"Extract object/array literals to stable references with `useMemo` or module-level constants, then pass those variables as props",
"rendering-hydration-no-flicker":
"Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",

Expand Down
6 changes: 6 additions & 0 deletions packages/react-doctor/tests/run-oxlint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ describe("runOxlint", () => {
fixture: "performance-issues.tsx",
ruleSource: "rules/performance.ts",
},
"no-inline-object-props": {
fixture: "performance-issues.tsx",
ruleSource: "rules/performance.ts",
severity: "warning",
category: "Performance",
},
"no-usememo-simple-expression": {
fixture: "performance-issues.tsx",
ruleSource: "rules/performance.ts",
Expand Down