diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts
index c2e8b15e9..7e741c0f8 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts
@@ -118,6 +118,34 @@ ruleTester.run(RULE_NAME, rule, {
code: /* tsx */ `<>{moo}>`,
errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
},
+ {
+ code: /* tsx */ `<>{moo}>`,
+ errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
+ options: [{ allowExpressions: false }],
+ },
+ {
+ code: /* tsx */ `<>{moo}>`,
+ errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
+ options: [{ allowExpressions: false }],
+ },
+ {
+ code: /* tsx */ `<>{moo}>`,
+ errors: [{ type: AST_NODE_TYPES.JSXElement, messageId: "noUselessFragment" }, {
+ type: AST_NODE_TYPES.JSXFragment,
+ messageId: "noUselessFragment",
+ }],
+ options: [{ allowExpressions: false }],
+ },
+ {
+ code: /* tsx */ `baz>}/>`,
+ errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
+ options: [{ allowExpressions: false }],
+ },
+ {
+ code: /* tsx */ `<>>`,
+ errors: [{ type: AST_NODE_TYPES.JSXFragment, messageId: "noUselessFragment" }],
+ options: [{ allowExpressions: false }],
+ },
],
valid: [
...allValid,
@@ -178,5 +206,13 @@ ruleTester.run(RULE_NAME, rule, {
},
},
},
+ {
+ code: /* tsx */ `{foo}`,
+ options: [{ allowExpressions: false }],
+ },
+ {
+ code: /* tsx */ `>} />`,
+ options: [{ allowExpressions: false }],
+ },
],
});
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts
index b55e2ebf7..2f3a43ec0 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts
@@ -18,6 +18,7 @@ export type MessageID =
function check(
node: TSESTree.JSXElement | TSESTree.JSXFragment,
context: RuleContext,
+ allowExpressions: boolean,
) {
const initialScope = context.sourceCode.getScope(node);
if (JSX.isKeyedElement(node, initialScope)) return;
@@ -26,7 +27,15 @@ function check(
const isChildren = AST.isOneOf([AST_NODE_TYPES.JSXElement, AST_NODE_TYPES.JSXFragment])(node.parent);
const [firstChildren] = node.children;
// ee eeee eeee ...>} />
- if (node.children.length === 1 && JSX.isLiteral(firstChildren) && !isChildren) return;
+ if (allowExpressions && node.children.length === 1 && JSX.isLiteral(firstChildren) && !isChildren) return;
+ if (!allowExpressions && isChildren) {
+ // <>hello, world>
+ return context.report({ messageId: "noUselessFragment", node });
+ } else if (!allowExpressions && !isChildren && node.children.length === 1) {
+ // const foo = <>{children}>;
+ // return <>{children}>;
+ return context.report({ messageId: "noUselessFragment", node });
+ }
const nonPaddingChildren = node.children.filter((child) => !JSX.isPaddingSpaces(child));
if (nonPaddingChildren.length > 1) return;
if (nonPaddingChildren.length === 0) return context.report({ messageId: "noUselessFragment", node });
@@ -37,7 +46,13 @@ function check(
context.report({ messageId: "noUselessFragment", node });
}
-export default createRule<[], MessageID>({
+type Options = [
+ {
+ allowExpressions: boolean;
+ },
+];
+
+export default createRule({
meta: {
type: "problem",
docs: {
@@ -47,19 +62,30 @@ export default createRule<[], MessageID>({
noUselessFragment: "A fragment contains less than two children is unnecessary.",
noUselessFragmentInBuiltIn: "A fragment placed inside a built-in component is unnecessary.",
},
- schema: [],
+ schema: [{
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ allowExpressions: {
+ type: "boolean",
+ },
+ },
+ }],
},
name: RULE_NAME,
- create(context) {
+ create(context, [option]) {
+ const { allowExpressions = true } = option;
return {
JSXElement(node) {
if (!isFragmentElement(node, context)) return;
- check(node, context);
+ check(node, context, allowExpressions);
},
JSXFragment(node) {
- check(node, context);
+ check(node, context, allowExpressions);
},
};
},
- defaultOptions: [],
+ defaultOptions: [{
+ allowExpressions: true,
+ }],
});
diff --git a/website/pages/docs/rules/no-useless-fragment.mdx b/website/pages/docs/rules/no-useless-fragment.mdx
index 5a5ffdc3c..f4be93f2c 100644
--- a/website/pages/docs/rules/no-useless-fragment.mdx
+++ b/website/pages/docs/rules/no-useless-fragment.mdx
@@ -67,10 +67,10 @@ const cat = <>meow>
## Note
-[This rule always allows single expressions in a fragment](https://github.com/Rel1cx/eslint-react/pull/188). This is useful in
+[By default, this rule always allows single expressions in a fragment](https://github.com/Rel1cx/eslint-react/pull/188). This is useful in
places like Typescript where `string` does not satisfy the expected return type
of `JSX.Element`. A common workaround is to wrap the variable holding a string
-in a fragment and expression.
+in a fragment and expression. To change this behaviour, use the `allowExpressions` option.
### Examples of correct code for single expressions in fragments:
@@ -80,6 +80,59 @@ in a fragment and expression.
{foo}
```
+## Examples with `allowExpressions: false`
+
+### Failing
+
+```tsx
+<>>
+
+<>foo>
+
+<>>
+
+baz>} />
+
+
+
+const cat = <>meow>
+
+<>{children}>
+
+<>{props.children}>
+
+<> {foo}>
+
+
+ <>
+
+
+ >
+
+```
+
+### Passing
+
+```tsx
+{foo}
+
+
+
+<>
+
+
+>
+
+<>foo {bar}>
+
+{item.value}
+```
+
## Further Reading
- [React: Fragment](https://react.dev/reference/react/Fragment)