diff --git a/README.md b/README.md
index defb494..e056cf3 100644
--- a/README.md
+++ b/README.md
@@ -175,3 +175,16 @@ If your using JSX inside `.js` files (which I don't recommend because it forces
"react-refresh/only-export-components": ["error", { "checkJS": true }]
}
```
+
+### customHOCs (v0.4.15)
+
+If you're exporting a component wrapped in a custom HOC, you can use this option to avoid false positives.
+
+```json
+{
+ "react-refresh/only-export-components": [
+ "error",
+ { "customHOCs": ["observer", "withAuth"] }
+ ]
+}
+```
diff --git a/src/only-export-components.test.ts b/src/only-export-components.test.ts
index b5accd9..1bfc26a 100755
--- a/src/only-export-components.test.ts
+++ b/src/only-export-components.test.ts
@@ -189,6 +189,11 @@ const valid = [
name: "Only React context",
code: "export const MyContext = createContext('test');",
},
+ {
+ name: "Custom HOCs like mobx's observer",
+ code: "const MyComponent = () => {}; export default observer(MyComponent);",
+ options: [{ customHOCs: ["observer"] }],
+ },
];
const invalid = [
@@ -295,6 +300,11 @@ const invalid = [
code: "export const MyComponent = () => {}; export const MyContext = React.createContext('test');",
errorId: "reactContext",
},
+ {
+ name: "should be invalid when custom HOC is used without adding it to the rule configuration",
+ code: "const MyComponent = () => {}; export default observer(MyComponent);",
+ errorId: ["localComponents", "anonymousExport"],
+ },
];
const it = (name: string, cases: Parameters[2]) => {
@@ -322,7 +332,9 @@ for (const { name, code, errorId, filename, options = [] } of invalid) {
{
filename: filename ?? "Test.jsx",
code,
- errors: [{ messageId: errorId }],
+ errors: Array.isArray(errorId)
+ ? errorId.map((messageId) => ({ messageId }))
+ : [{ messageId: errorId }],
options,
},
],
diff --git a/src/only-export-components.ts b/src/only-export-components.ts
index b74a285..e85948c 100644
--- a/src/only-export-components.ts
+++ b/src/only-export-components.ts
@@ -21,6 +21,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
allowConstantExport?: boolean;
checkJS?: boolean;
allowExportNames?: string[];
+ customHOCs?: string[];
},
]
> = {
@@ -47,6 +48,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
allowConstantExport: { type: "boolean" },
checkJS: { type: "boolean" },
allowExportNames: { type: "array", items: { type: "string" } },
+ customHOCs: { type: "array", items: { type: "string" } },
},
additionalProperties: false,
},
@@ -58,6 +60,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
allowConstantExport = false,
checkJS = false,
allowExportNames,
+ customHOCs = [],
} = context.options[0] ?? {};
const filename = context.filename;
// Skip tests & stories files
@@ -79,6 +82,16 @@ export const onlyExportComponents: TSESLint.RuleModule<
? new Set(allowExportNames)
: undefined;
+ const reactHOCs = new Set(["memo", "forwardRef", ...customHOCs]);
+ const canBeReactFunctionComponent = (init: TSESTree.Expression | null) => {
+ if (!init) return false;
+ if (init.type === "ArrowFunctionExpression") return true;
+ if (init.type === "CallExpression" && init.callee.type === "Identifier") {
+ return reactHOCs.has(init.callee.name);
+ }
+ return false;
+ };
+
return {
Program(program) {
let hasExports = false;
@@ -298,16 +311,6 @@ export const onlyExportComponents: TSESLint.RuleModule<
},
};
-const reactHOCs = new Set(["memo", "forwardRef"]);
-const canBeReactFunctionComponent = (init: TSESTree.Expression | null) => {
- if (!init) return false;
- if (init.type === "ArrowFunctionExpression") return true;
- if (init.type === "CallExpression" && init.callee.type === "Identifier") {
- return reactHOCs.has(init.callee.name);
- }
- return false;
-};
-
type ToString = T extends `${infer V}` ? V : never;
const notReactComponentExpression = new Set<
ToString