Closed cburgmer closed 2 years ago
This is a tricky one, because if it’s any falsy value besides a number, it will work properly.
Once the nullish coalescing operator lands, and that’s a better alternative, it might make more sense to have a rule - right now, such a rule would probably be more noisy than helpful, I’m afraid.
I managed to miss this issue, and implemented a rule as suggested here by @cburgmer in PR #2077.
It protects solely against using the truthiness of a length prop as guard, which as @ljharb said in the PR might be too narrow to be useful.
I don’t see an immediately obvious way of expanding the rule to cover more numeric guard cases, but my gut feeling is that the .length case is common enough to justify the rule.
Then again, it might be that I want to believe that just so I’m not alone in being a schmuck (did exactly this mistake in production code twice)...
Relying on the implicit truthiness of .length
anywhere imo is a bad idea (for this reason as well as many others) - if you always do a strict number comparison with .length
, you won't run into the problem there at all (which the airbnb styleguide requires, ftr).
Hmm. Interesting. I was focused on the fact that implicit length truthiness is especially dangerous in JSX, earning me the title ”0 of the week” on the main company whiteboard.
But I see your point - you’ll live a happier life if you never rely on it, jsx or not.
Still, in most other environments it’s more arrogant than unsafe to do it, no? There I’d classify this rule as ”coding style”, while in JSX it’s a ”potential error” (actually pretty much a guaranteed one).
@ljharb, would you consider this rule if we add a ”forbidEverywhere” option, disallowing boolean casting of dot length anywhere as per the airbnb styleguide?
Another way of making the rule more useful could be to allow the user to add props other than .length
to also be included in the check. Maybe I'm using lots of Set
:s in my codebase and could thus add .size
, for example.
I don't think it's really something a linter can do - there's no guarantee a length property is even a number (not that I can think of a use case for that). I think at some point the solution is always going to boil down to actual tests.
A linter would've saved me, twice! But then again, so would writing better code. :)
I agree there's no guarantee, but I think the argument is that there doesn't need to be, and that erroneous casting of .length
is common enough to warrant a rule.
Also, protecting against this particular mistake with tests seems a bit off - would you really write a test to see that you didn't render a zero?
But I'm liking the idea of enforcing the AirBnB rule of never casting length to boolean, and so we'll probably widen the rule to do that and deploy it to our linting suite outside of the React package.
Thank you for the input!
I might have misunderstood, the only linting rule I've sound so far is https://github.com/sindresorhus/eslint-plugin-unicorn/blob/master/docs/rules/explicit-length-check.md, which is not included in the Airbnb rule set.
To reproduce:
$ npm install eslint-config-airbnb-base
$ npx install-peerdeps --dev eslint-config-airbnb-base
$ cat > h.js << EOF
heredoc> const list = [];
if (list.length) {
list.pop();
}
heredoc> EOF
$ npx eslint .
$
@ljharb: I want to make a last attempt at convincing you!
In the 2.5 months passed I've seen the arr.length && <Something/>
mistake thrice more in the wild. Yes, a rule that could catch all numeric guards would be better, and a rule that could catch all non-boolean guards would be better still. But I think the flawed arr.length
usage is common enough to warrant a dedicated rule. It might not be a broad rule, but I argue it'll save more than enough grief to pay for the inclusion.
To me the bottom line is this: we have a very common pattern in normal JS that might be frowned upon by some, but it is still widely used and perfectly safe. In JSX it suddenly isn't safe anymore.
So yes, you could argue that users should use a general explicit-length-check rule like the one linked by @cburgmer above. But to me that rule enforces an opinion, which falls in a different category. The proposed jsx rule catches the usage in JSX specifically, which is always a mistake.
Half a year later, new assignment, ran this rule on the (large) codebase. Found 10 violations, all of which were real dangers of rendering a 0 to screen. Codebase is otherwise in very good shape with sound linting, and the devs are experienced and driven.
I'm even more convinced than before that this rule deserves a place in a React-dedicated rules package.
I managed to miss this issue, and implemented a rule as suggested here by @cburgmer in PR #2077.
By the way, how to config make it take effect? :smile:
I'd like a rule for that as well. I'm not sure if this package treats react-native as first class citizen, but if it does, another point to consider is that a lint rule like this would actually prevent errors in react-native. Unlike in the browser, you can't render text outside of <Text/>
components. If you do, your app crashes. Another source of errors are cases like this:
{
props.text && <Text>{props.text}</Text>
}
This would just render an empty string and just won't affect a react app. In react native however, we'd be trying to render a string outside of a Text component too.
Half a year later, new assignment, ran this rule on the (large) codebase.
Where is the rule? You refer to the length
one?
Will a more broader rule be considered? Especially in the react-native env such rule would help to prevent serious bugs in the code base. Maybe it could only be implemented as a typescript rule, because the TS compiler knows about types (and RN projects are usually type script projects).
I've seen eslint rules in conjunction with TypeScript. Maybe the rule I'm looking will benefit from the type information. Would such a rule still be welcome here?
@cburgmer as long as it's still useful with no type information, absolutely.
One solution is to use "@typescript-eslint/strict-boolean-expressions": ["error", { "allowNumber": false }]
.
Alternative solution which doesn't require types would be to create a rule which disallows a && b
in JSX altogether and always autofixes it to a ? b : null
that wouldn’t be appropriate tho when a is a boolean type :-/
Why though?
I often use ternary in JSX even when the else branch is just null. I like it because it's consistent. It also kinda forces you to at least consider displaying some message like "No data to show" which is usually a nice UX. I once had to refactor a bunch of components to show these empty states in UI and if I used ternaries from the beginning it would be much easier. It would be useful rule for enforcing style, and if you want actual bug-catching rule then there's strict-boolean-expressions.
"a nice UX" depends very much on the context.
a ? b : null
Just to share a painful learning from today: The null
here is rather important, and ""
should be avoided, although it looks the same to the user. What happens is that for ""
React will render an empty text node, which in our case broke our xpath query we are using for our browser-based tests. //span[contains(text(), "my caption"]
will not behave as expected if an empty text node is created alongside the caption:
The following JSX
<span>
{false ? "whatever : ""}
"my caption"
<span>
will result in this HTML (quotes used to highlight the text nodes)
<span>
""
"my caption"
</span>
@cburgmer to be fair tho, relying on XPath queries like that is inherently brittle, so the flaw there isn't use of the empty string.
Would very much like a rule that enforces ternary usage over &&
for conditional rendering in JSX.
To enforce ternary in JSX you can use no-restricted-syntax
like this:
{
"no-restricted-syntax": [
"error",
{
"selector": "JSXElement > JSXExpressionContainer > LogicalExpression[operator!='??']",
"message": "Please use ternary operator instead"
}
]
}
Just adding to @phaux's suggestion above:
{
"no-restricted-syntax": [
"error",
{
"selector": "JSXElement,JSXFragment > JSXExpressionContainer > LogicalExpression[operator!='??']",
"message": "Please use ternary operator instead"
}
]
}
@marneborn your example does not work. Try this:
{
"no-restricted-syntax": [
"error",
{
"selector": ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > LogicalExpression[operator!='??']",
"message": "Please use ternary operator instead"
}
]
}
It would still be great to have a rule to detect non-boolean type for inline rendering, without needing to enforce ternary expressions. I thought the goal was to spot code like this:
<div>
{unreadMessages.length && <h2>Hey</h2>}
</div>
rather than banning any logical expression with && in JSX. Is there any way to achieve this with custom rules?
@alesmit I have an implementation of exactly that in #2077 (which was shot down)
The goal is to spot any chance for a 0
to be rendered. .length
isn't the only one, and it isn't a reliably detectable one without type information.
Implementing a proper boolean check is quite problematic indeed. It requires both TypeScript and React contexts.
In the meantime, I've added this rule to our .eslintrc.js
, which is an enhanced version for the previous suggestions [1], [2]:
"no-restricted-syntax": [
"error",
...otherRules,
// Two rules below help us avoid this common point of confusion: https://stackoverflow.com/q/53048037
// The selectors are inspired by https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-844344470
{
selector:
":matches(JSXElement, JSXFragment) > JSXExpressionContainer > LogicalExpression[operator='&&']",
message:
"Please use `condition ? <Jsx /> : undefined`. Otherwise, there is a chance of rendering '0' instead of '' in some cases. Context: https://stackoverflow.com/q/53048037",
},
{
selector:
":matches(JSXElement, JSXFragment) > JSXExpressionContainer > LogicalExpression[operator='||']",
message:
"Please use `value ?? fallbackValue`. Otherwise, there is a chance of rendering '0' instead of '' in some cases. Context: https://stackoverflow.com/q/53048037",
},
],
The messages are meant to help folks who are relatively new in React or JS as a whole. Showing the expected syntax and explaining the pitfall makes the rule easier to understand.
Hope it helps!
For what it's worth, this is an even more severe issue on React Native. If you accidentally return a value like 0
where there's supposed to be a component, it throws a (confusing) error that breaks your app instead of just rendering an unexpected value (for example, see https://stackoverflow.com/questions/52368342/invariant-violation-text-strings-must-be-rendered-within-a-text-component/59108109#59108109)
If we can come up with a rule that by default assumes React, but has an option to go into "React Native" mode - and that rule provides no false positives, and still provides some value - then I think we might have a candidate here.
Can someone sum up what that might look like?
Ended up encountering this issue and writing a rule for it. For those using TypeScript, you can try using jsx-expressions/strict-logical-expressions
@hpersson this is fantastic!! I've been looking for a rule for this for a long time and was about to start writing it myself. It looks like a minor issue but the mistake is so easily made, and in React Native it can even cause a crash due to the Text strings must be rendered within a <Text> component
error. Thank you!
@hpersson if your rule can be made to match the expectations in https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-901577684, a PR would be great.
Created a babel plugin to automatically fix this error in a codebase
steps to use
npm i -D @babel/cli
update babel config to include local plugin
module.exports = function (api) {
api.cache(true);
const presets = [];
const plugins = ['./ternary-jsx.js'];
return {
parserOpts: {
plugins: ['jsx', 'typescript'],
},
retainLines: true,
presets,
plugins,
};
};
npx babel src -x ".tsx" --out-dir src --keep-file-extension
I want to reference @kentcdodds' article about this too, as I think it can be helpful in this discussion
https://kentcdodds.com/blog/use-ternaries-rather-than-and-and-in-jsx
I think it should be fairly simple to create a rule which:
||
or &&
direcly inside a JSX expression node (basically https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-864168062 with an autofix)
<div>
{foo && bar}
{foo || bar}
<div>
autofix:
<div>
{foo ? bar : null}
{foo ? null : bar}
<div>
||
or &&
const foo = bar && <div />
const foo = bar || <div />
autofix:
const foo = bar ? <div /> : null
const foo = bar ? null : <div />
The rule could be named react/jsx-no-logical-expressions
Of course this doesn’t catch all scenarios, but I think it catches a significant amount.
If @ljharb is open to this, I’ll create a pull request.
It might be better to use undefined
instead of null
in autofixes for compatibility with unicorn/no-null
.
<div>
- {foo ? bar : null}
+ {foo ? bar : undefined}
- {foo ? null : bar}
+ {foo ? undefined : bar}
<div>
No, that wouldn't be better; "no null" is an absurd rule and philosophy.
@remcohaszing I don't think that's a reasonable approach, since {foo && bar}
and {foo || bar}
are both perfectly reasonable and acceptable if you're using react web, and if foo
is not a zero.
No, that wouldn't be better; "no null" is an absurd rule and philosophy.
Hmm I’m not sure if ‘absurd’ is the right term for this opinion in code style... Could you please elaborate?
According to official React docs, null
and undefined
are equally valid:
Advantages of not using null
are summarised in unicorn/no-null
→ Why? section.
@kachkaev i'm talking about the "unicorn" rule.
Also, undefined
is only a valid return from components in very recent versions of React; null
is the safer choice for backwards compatibility.
I disagree with all of those advantages, including the "intent" post, and that's what I'm saying is absurd.
Thanks for sharing your thoughts, but I’m still not convinced 😅 Repeating ’this is absurd’ but not providing any links or arguments does help others find flaws in their thinking. You don’t have to prove anything to me, but folks in this thread may be interested in finding out more on this subject 👀
Regardless of unicorn/no-null
, usage of undefined
in JSX ternaries still seems to have merit. Perhaps, there is scope for an option in the new potential rule.
@kachkaev null
is an important part of the language. undefined
triggers defauls, null
doesn't. I will repeat that it is absurd to suggest not using a critical language primitive that has distinct semantics.
Since I don't think the "change to ternaries" approach is viable anyways, it's not really worth the time to debate null vs undefined in it here.
We can always implement what @remcohaszing suggested in https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-1025782557, but we don't need to make it recommended.
People can than enable the rule if they want to.
@MichaelDeBoey "what's recommended" isn't particularly relevant; providing a rule at all is a tacit endorsement of enabling it, so rules that shouldn't exist, are best not implemented (require-await
, for example, being a core rule whose mere existence harms the ecosystem).
This new rule could be enabled if they want to make sure they never make the mistake with a 0
rendered without them knowing, so I think this rule can be beneficial for a lot of people tbh.
@MichaelDeBoey i understand the value, but i think it's too blunt an instrument.
If we could use propType or TS type information to know with certainty when a zero is possible, that might be worth looking into.
Such a rule would also need an option for react native users, to widen the list of "bad values" from zero to whatever RN is unable to render.
There is https://github.com/yannickcr/eslint-plugin-react/issues/2073#issuecomment-945025341 for TS users already. AFAIU this package cannot leverage typings because it is JS-only.
@kachkaev that's right, but it can leverage propTypes, which are much more powerful than TS is for determining the type of a thing (altho the limits of static analysis do constrain what we can do here)
FWIW the Typescript rule works like a charm. So I'm not sure which limits you are referring to
We are making use of inline conditional guard statements a lot like:
We often run into bugs where a string literal
0
is rendered, because a short hand notion was used, which react does not allow:React will not render a boolean value, while it does for integer values like 0 and 1.
If possible, I'd like to detect this pattern and flag it early. Apologies if this already exists, I checked for "conditional" and "inline" and couldn't find a match.