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
3 changes: 3 additions & 0 deletions packages/eslint-config/base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import reactStrictStructure from "eslint-plugin-react-strict-structure";
import strictEnv from "eslint-plugin-strict-env";
import turboPlugin from "eslint-plugin-turbo";
import tseslint from "typescript-eslint";
Expand All @@ -23,9 +24,11 @@ export const config = [
},
{
plugins: {
"react-strict-structure": reactStrictStructure,
"strict-env": strictEnv,
},
rules: {
"react-strict-structure/no-inline-jsx-functions": "error",
"strict-env/no-process-env": "error",
},
},
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"eslint-plugin-only-warn": "^1.1.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-strict-structure": "workspace:*",
"eslint-plugin-strict-env": "workspace:*",
"eslint-plugin-turbo": "^2.5.5",
"globals": "^15.15.0",
Expand Down
20 changes: 20 additions & 0 deletions packages/eslint-plugin-react-strict-structure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# eslint-plugin-react-strict-structure

A custom ESLint plugin to enforce strict React performance patterns. Specifically, it prevents the definition of inline functions that return JSX inside components.

## Why use this?

Defining a function that returns JSX _inside_ another component (often called a "render helper") is a React anti-pattern.

```javascript
// ❌ BAD
function Parent() {
// This function is re-created on every render
const RenderItem = () => <div>Item</div>;

// React treats <RenderItem /> as a BRAND NEW component type every time.
// Result: It unmounts the old one, mounts the new one.
// Side effects: Loss of state, loss of focus, high memory churn.
return <RenderItem />;
}
```
15 changes: 15 additions & 0 deletions packages/eslint-plugin-react-strict-structure/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { noInlineJsxFunctions } from "./rules/no-inline-jsx-functions.js";

export default {
rules: {
'no-inline-jsx-functions': noInlineJsxFunctions,
},
configs: {
recommended: {
plugins: ['react-strict-structure'],
rules: {
'react-strict-structure/no-inline-jsx-functions': 'error',
},
},
},
};
21 changes: 21 additions & 0 deletions packages/eslint-plugin-react-strict-structure/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "eslint-plugin-react-strict-structure",
"version": "0.0.1",
"type": "module",
"main": "index.js",
"scripts": {
"test": "node --test src/rules/no-inline-jsx-functions.test.js"
},
"keywords": [
"eslint",
"eslint-plugin",
"eslint-plugin-react",
"eslint-plugin-react-jsx"
],
"peerDependencies": {
"eslint": ">=9.0.0"
},
"devDependencies": {
"eslint": "^9.39.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
export const noInlineJsxFunctions = {
meta: {
type: "problem",
docs: {
description:
"Prevent definition of inline functions that return JSX inside components",
category: "Best Practices",
recommended: true,
},
schema: [],
messages: {
noInlineJsx:
"Avoid defining functions that return JSX inside components. Move this to a separate component or outside the body.",
},
},
create(context) {
return {
// Check Function Declarations, Expressions, and Arrow Functions
":function"(node) {
if (!parentFunction(parent(node.parent))) {
// It's a top-level component, which is fine
return;
}

if (isReturnJSX(node.body)) {
context.report({
node,
messageId: "noInlineJsx",
});
}
},
};
},
};

function parent(nodeParent) {
let parent = nodeParent;
while (parent) {
if (isFunction(parent)) {
return parent;
}
parent = parent.parent;
}

return parent;
}

function parentFunction(parent) {
while (parent) {
if (isFunction(parent)) {
return parent;
}
}

return null;
}

function isReturnJSX(nodeBody) {
const { body, type } = nodeBody;
if (type === "BlockStatement") {
// Look for 'return <JSX />' inside the function body
for (const { argument, type } of body) {
if (type === "ReturnStatement" && isJSX(argument)) {
return true;
}
}

return false;
}

if (isJSX(nodeBody)) {
// Implicit arrow return: () => <div />
return true;
}

return false;
}

function isFunction(node) {
if (!node) {
return false;
}

return [
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
].includes(node.type);
}

function isJSX(node) {
if (!node) {
return false;
}

const { type } = node;
return type === "JSXElement" || type === "JSXFragment";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { RuleTester } from "eslint";
import { noInlineJsxFunctions } from "./no-inline-jsx-functions.js";

new RuleTester({
languageOptions: {
ecmaVersion: 2020,
sourceType: "module",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
}).run("no-inline-jsx-functions", noInlineJsxFunctions, {
valid: [
{
code: `function MyComponent() { return <div>Hello</div>; }`,
},
{
code: `const Helper = () => <span>Helper</span>; function MyComponent() { return <Helper />; }`,
},
],
invalid: [
{
code: `
function MyComponent() {
function NestedHelper() {
return <div>Bad</div>;
}
return <NestedHelper />;
}
`,
errors: [{ messageId: "noInlineJsx" }],
},
{
code: `
const MyComponent = () => {
const renderItem = () => <span>Item</span>;
return <div>{renderItem()}</div>;
};
`,
errors: [{ messageId: "noInlineJsx" }],
},
],
});
Loading