Skip to content

Commit 9f1d618

Browse files
Matt Darvenizaljharb
authored andcommitted
[New] jsx-no-useless-fragments: add option to allow single expressions in fragments
1 parent 860ebea commit 9f1d618

File tree

4 files changed

+57
-16
lines changed

4 files changed

+57
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
55

66
## Unreleased
77

8+
### Added
9+
* [`jsx-no-useless-fragments`]: add option to allow single expressions in fragments ([#3006][] @mattdarveniza)
10+
811
### Fixed
912
* component detection: use `estraverse` to improve component detection ([#2992][] @Wesitos)
1013
* [`destructuring-assignment`], [`no-multi-comp`], [`no-unstable-nested-components`], component detection: improve component detection ([#3001][] @vedadeepta)
1114

1215
### Changed
1316
* [Docs] [`jsx-no-bind`]: updates discussion of refs ([#2998][] @dimitropoulos)
1417

18+
[#3006]: https://github.com/yannickcr/eslint-plugin-react/pull/3006
1519
[#3001]: https://github.com/yannickcr/eslint-plugin-react/pull/3001
1620
[#2998]: https://github.com/yannickcr/eslint-plugin-react/pull/2998
1721
[#2992]: https://github.com/yannickcr/eslint-plugin-react/pull/2992

docs/rules/jsx-no-useless-fragment.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,16 @@ const cat = <>meow</>
5252

5353
<Fragment key={item.id}>{item.value}</Fragment>
5454
```
55+
56+
### `allowExpressions`
57+
58+
When `true` single expressions in a fragment will be allowed. This is useful in
59+
places like Typescript where `string` does not satisfy the expected return type
60+
of `JSX.Element`. A common workaround is to wrap the variable holding a string
61+
in a fragment and expression.
62+
63+
Examples of **correct** code for the rule, when `"allowExpressions"` is `true`:
64+
65+
```jsx
66+
<>{foo}</>
67+
```

lib/rules/jsx-no-useless-fragment.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ function containsCallExpression(node) {
7777
&& node.expression.type === 'CallExpression';
7878
}
7979

80+
function isFragmentWithSingleExpression(node) {
81+
return node && node.children.length === 1 && node.children[0].type === 'JSXExpressionContainer';
82+
}
83+
8084
module.exports = {
8185
meta: {
8286
type: 'suggestion',
@@ -88,12 +92,15 @@ module.exports = {
8892
url: docsUrl('jsx-no-useless-fragment')
8993
},
9094
messages: {
91-
NeedsMoreChidren: 'Fragments should contain more than one child - otherwise, there‘s no need for a Fragment at all.',
95+
NeedsMoreChildren: 'Fragments should contain more than one child - otherwise, there‘s no need for a Fragment at all.',
9296
ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.'
9397
}
9498
},
9599

96100
create(context) {
101+
const config = context.options[0] || {};
102+
const allowExpressions = config.allowExpressions || false;
103+
97104
const reactPragma = pragmaUtil.getFromContext(context);
98105
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
99106

@@ -198,10 +205,14 @@ module.exports = {
198205
return;
199206
}
200207

201-
if (hasLessThanTwoChildren(node) && !isFragmentWithOnlyTextAndIsNotChild(node)) {
208+
if (
209+
hasLessThanTwoChildren(node)
210+
&& !isFragmentWithOnlyTextAndIsNotChild(node)
211+
&& !(allowExpressions && isFragmentWithSingleExpression(node))
212+
) {
202213
context.report({
203214
node,
204-
messageId: 'NeedsMoreChidren',
215+
messageId: 'NeedsMoreChildren',
205216
fix: getFix(node)
206217
});
207218
}

tests/lib/rules/jsx-no-useless-fragment.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,43 +67,48 @@ ruleTester.run('jsx-no-useless-fragment', rule, {
6767
{
6868
code: '<>{foos.map(foo => foo)}</>',
6969
parser: parsers.BABEL_ESLINT
70+
},
71+
{
72+
code: '<>{moo}</>',
73+
parser: parsers.BABEL_ESLINT,
74+
options: [{allowExpressions: true}]
7075
}
7176
],
7277
invalid: [
7378
{
7479
code: '<></>',
7580
output: null,
76-
errors: [{messageId: 'NeedsMoreChidren', type: 'JSXFragment'}],
81+
errors: [{messageId: 'NeedsMoreChildren', type: 'JSXFragment'}],
7782
parser: parsers.BABEL_ESLINT
7883
},
7984
{
8085
code: '<>{}</>',
8186
output: null,
82-
errors: [{messageId: 'NeedsMoreChidren', type: 'JSXFragment'}],
87+
errors: [{messageId: 'NeedsMoreChildren', type: 'JSXFragment'}],
8388
parser: parsers.BABEL_ESLINT
8489
},
8590
{
8691
code: '<p>moo<>foo</></p>',
8792
output: '<p>moofoo</p>',
88-
errors: [{messageId: 'NeedsMoreChidren'}, {messageId: 'ChildOfHtmlElement'}],
93+
errors: [{messageId: 'NeedsMoreChildren'}, {messageId: 'ChildOfHtmlElement'}],
8994
parser: parsers.BABEL_ESLINT
9095
},
9196
{
9297
code: '<>{meow}</>',
9398
output: null,
94-
errors: [{messageId: 'NeedsMoreChidren'}],
99+
errors: [{messageId: 'NeedsMoreChildren'}],
95100
parser: parsers.BABEL_ESLINT
96101
},
97102
{
98103
code: '<p><>{meow}</></p>',
99104
output: '<p>{meow}</p>',
100-
errors: [{messageId: 'NeedsMoreChidren'}, {messageId: 'ChildOfHtmlElement'}],
105+
errors: [{messageId: 'NeedsMoreChildren'}, {messageId: 'ChildOfHtmlElement'}],
101106
parser: parsers.BABEL_ESLINT
102107
},
103108
{
104109
code: '<><div/></>',
105110
output: '<div/>',
106-
errors: [{messageId: 'NeedsMoreChidren'}],
111+
errors: [{messageId: 'NeedsMoreChildren'}],
107112
parser: parsers.BABEL_ESLINT
108113
},
109114
{
@@ -115,12 +120,12 @@ ruleTester.run('jsx-no-useless-fragment', rule, {
115120
output: `
116121
<div/>
117122
`,
118-
errors: [{messageId: 'NeedsMoreChidren'}],
123+
errors: [{messageId: 'NeedsMoreChildren'}],
119124
parser: parsers.BABEL_ESLINT
120125
},
121126
{
122127
code: '<Fragment />',
123-
errors: [{messageId: 'NeedsMoreChidren'}]
128+
errors: [{messageId: 'NeedsMoreChildren'}]
124129
},
125130
{
126131
code: `
@@ -131,7 +136,7 @@ ruleTester.run('jsx-no-useless-fragment', rule, {
131136
output: `
132137
<Foo />
133138
`,
134-
errors: [{messageId: 'NeedsMoreChidren'}]
139+
errors: [{messageId: 'NeedsMoreChildren'}]
135140
},
136141
{
137142
code: `
@@ -145,19 +150,19 @@ ruleTester.run('jsx-no-useless-fragment', rule, {
145150
fragment: 'SomeFragment'
146151
}
147152
},
148-
errors: [{messageId: 'NeedsMoreChidren'}]
153+
errors: [{messageId: 'NeedsMoreChildren'}]
149154
},
150155
{
151156
// Not safe to fix this case because `Eeee` might require child be ReactElement
152157
code: '<Eeee><>foo</></Eeee>',
153158
output: null,
154-
errors: [{messageId: 'NeedsMoreChidren'}],
159+
errors: [{messageId: 'NeedsMoreChildren'}],
155160
parser: parsers.BABEL_ESLINT
156161
},
157162
{
158163
code: '<div><>foo</></div>',
159164
output: '<div>foo</div>',
160-
errors: [{messageId: 'NeedsMoreChidren'}, {messageId: 'ChildOfHtmlElement'}],
165+
errors: [{messageId: 'NeedsMoreChildren'}, {messageId: 'ChildOfHtmlElement'}],
161166
parser: parsers.BABEL_ESLINT
162167
},
163168
{
@@ -227,7 +232,15 @@ ruleTester.run('jsx-no-useless-fragment', rule, {
227232
</html>
228233
);
229234
`,
230-
errors: [{messageId: 'NeedsMoreChidren'}, {messageId: 'ChildOfHtmlElement'}]
235+
errors: [{messageId: 'NeedsMoreChildren'}, {messageId: 'ChildOfHtmlElement'}]
236+
},
237+
// Ensure allowExpressions still catches expected violations
238+
{
239+
code: '<><Foo>{moo}</Foo></>',
240+
options: [{allowExpressions: true}],
241+
errors: [{messageId: 'NeedsMoreChildren'}],
242+
output: '<Foo>{moo}</Foo>',
243+
parser: parsers.BABEL_ESLINT
231244
}
232245
]
233246
});

0 commit comments

Comments
 (0)