protip: you can use an IIFE (immediately invoked function expression) to put whatever conditionals or logic you want inside JSX! I use this all the time and it's much nicer for more complex blocks:
{(() => {
switch(state) {
case 'loading': return <Loading />;
case 'ready': return <Component />;
case 'error': return <Error error={error} />;
}
})()}
Micro-optimisations aren't useful. If you get to the point where the performance cost of inline functions is a bottleneck, you probably shouldn't be using React at all.
Try as I might, I have to reason through ternaries every time I encounter them. I think the = being so removed from what it's actually assigning, but without grouping parens, is what messes me up. Plus I always forget what the punctuation characters mean—if/elseif/else uses words, so I don't have to remember.
> I think the = being so removed from what it's actually assigning, but without grouping parens, is what messes me up.
I almost always use grouping parentheses for this reason, unless it’s a very short single line expression. That said, if/else if/else has a different colocation problem: it puts the assignment further from the initial declaration, making its scope less obvious (unless you’re hoisting var, which is awful for its own reasons).
Agree, the problem with if/else is that you end up having to consider the whole block plus usually a little context from just before it. I still find it much easier to read, unless very poorly written, while all ternaries slow me down every single time I read one, no matter how well-written. I struggle to parse them into ideas and words and to follow the order of events, seemingly no matter how many times I encounter them. They just feel wrong. All that implicit scoping crammed into one line, relying on memory and active searching to find the boundaries and then follow the order of events back "to the top", rather than having them explicitly marked.
While I do use this style occasionally, the previous commenter's point still stands that as value1, value2, fallback become longer than a couple of characters it does become very hard to read. It's also something that can start out fine but as the code grows more complex over time it can spiral into unreadability.
Especially if you format them well. Just use a newline before each `:` and maybe after each `?` (if the value expressions are large), and it'll look great.
This looks fine as an example right here, but it starts to look terrible if there's larger or multiline boolean expressions involved, while if/else if winds up still being pretty readable.
> Still a useful overview of common idioms though for people who just get started.
I agree - I just think labeling them as JSX issues is pretty disingenuous. JSX is literally just running them as standard js expressions here - which I actually find delightful: they work the same as they do in normal javascript (quirks included :D ).
The author seems to want something like ng-if="condition", and I find DSLs in this space a real abomination.
I've always found these approaches to writing conditions in JSX to be terrible. Wouldn't it be nice if JavaScript were an expression-oriented language? Then you could just write:
Yes, I'm well aware of how to use these. I will elaborate on why I think it's terrible (even though there is no alternative in standard JS yet):
* It encourages JSX-specific idioms. Outside of JSX, using `&&` instead of `if` for control flow would raise eyebrows from most people, I think.
* I find it easier and faster to refactor `if` statements to `if/else` and vice versa (only requires addition or deletion of code) than to refactor `&&` to a ternary operator and vice versa (also requires modifying existing code).
* Multiple nested ternary operators almost immediately become a mess, while a series of `else if` expressions (if such a thing existed) seem perfectly readable.
> Outside of JSX, using `&&` instead of `if` for control flow would raise eyebrows from most people, I think.
Very much so, at least for me. Relying on the short-circuiting of logical operators is fine, but only when you're actually going to use the resulting value. In the case of JSX, this is relying on the fact that `false` is a valid React child which renders nothing. Not only does this result in a mistake when the `&&` expression returns something like `0` that is falsey but isn't `false`, IMO it's already pretty awkward even if you are rendering `false`. I'd honestly prefer a runtime error, just like you get if you try to render a JS object, and only support rendering null and maybe undefined as React children.
> The React framework strives for catching everything at compile-time. Runtime errors are a big no-no in web development.
I don't know whether that principle is generally true or ought to be generally true, but React does throw a runtime error if you render a plain JS object as a React child. This can probably also be prevented at compile time with linters or TypeScript, but given that React has to do something at runtime if it encounters an invalid child, I think throwing an error is preferable to just rendering nothing or having some undefined behavior.
In my opinion, rendering `null` is a pretty clear and explicit way to indicate you don't want to render anything. But rendering `false` (or `true`, for that matter) is not at all so clear to me. I think throwing a runtime error would be better, and would largely make the `thing && <Component />` idiom go away.
I get what you are saying. In my experience, it ends up being moot when you are explicitly trying to avoid runtime errors, because you’ll need something along the lines of “guard() && <Component />” or you could simply have “<Component />” and then within Component render have “if (!guarded) return <Fragment />”, etc.
At that point, you’ll probably need to worry about component collections containing empty elements, though. That pulls you back into the parent scope, anyways.
There’s probably a nicer way to handle it with custom hooks, though.
> I don't know whether that principle is generally true or ought to be generally true
They sure do go out of their way to make misuse of hooks a compile-time error. I think that those useful error messages go a long way to rectifying the archaic semicolon error messages of the C days.
It’s generally better to go with the idioms of the language, sure, but in this case, the idioms of JSX work well and they are future-proof. If JavaScript adds the support that you are looking for, it would be easy enough for a static code analyzer to rewrite “&&” as “if”
What if you consider it as ‘only returns a data structure that requests the rendering of <Anything /> if someBool is true’?
If you write
console.log(someBool && “anything”)
Are you altering the control flow because the console window will call a different bit of font evaluation code to render “anything” instead of “false”?
Or are you just conditionally evaluating an expression that results in different data that causes different downstream effects?
Evaluating <Anything/> doesn’t do anything. It doesn’t make any DOM elements. It doesn’t trigger any useEffects. It just returns an object that has the potential to be hooked into a react renderDOM lifecycle to provide further instructions on what the DOM should look like and what other core should run.
I already responded down thread because I didn’t see this comment first. But I’ll add that your console log example is more analogous to the JSX control flow question as:
Okay: only evaluates <Anything /> if someBool is true. The value of someBool quite literally controls which code path is taken. It’s even more clear with a fallback:
{ someBool && <Anything /> || <Fallback /> }
Which would more idiomatically be written as a ternary conditional, but still. It doesn’t matter where the expression is placed, it’s the same if you assign it to a variable:
const el = someBool && <Anything /> || <Fallback />;
And the result (assuming well behaved react code) would be the same. <Anything /> is just a literal expression. It doesn’t matter whether it gets evaluated or not.
You’re not using && shortcircuiting to prevent <Anything/> from being evaluated - it doesn’t matter if it gets evaluated. You are using it to decide which value to return.
This really isn’t ‘using shortcircuiting for control flow’. It’s just using && as an operator in an expression evaluation.
Edit: I read this back to myself and realized it may come off as harsh or mean. I don’t intend it as such, and I apologize if it’s received that way. I think being precise about this is worth pursuing, for the sake of both developers thinking about it and users experiencing what we build.
- - -
Of course it matters.
“Well behaved react code” isn’t side-effect free, as much as it tries hard to look almost like it is. You can write perfectly idiomatic components which follow all best practices, and evaluating a component when you don’t intend to use it will still:
1. Execute the code (duh), which at minimum uses CPU resources
2. Perform relevant hooks and call lifecycle methods, including those which have side effects (hint: “effect” is in the hook’s name, for this reason): starting timers, loading resources, calling non-React APIs, pretty much anything goes by design, even if you follow all the rules and other best practices.
3. Prepare for a new, potentially concurrent, render. This may speculatively interrupt/pause a render in progress which would otherwise complete without interruption. Which may, in turn, cause seemingly unrelated components to be called again, cascading this entire set of potential side effects to them, and so on.
4. Call any components in that cascade whether they’re well behaved in your control, or epic shit show dependencies.
Being in an expression position does not mean it’s side effect free. If you don’t believe me, run this code:
const isPure = (value) => (
value = 'NOPE',
console.log('There is no code block in sight, this function has zero statements. Is it pure?', value),
value
);
isPure('yeah?');
And sure, React and most JSX implementations are designed to make evaluating JSX side effects local as much as possible. But that’s limited because it’s exposed to APIs not under its control by design, first of all. And more importantly, I would hope that, on HN of all places, a knowable answer to a factual question “is code executed?” is not treated as subjective. This is the exact same question as “is it control flow?”
> 1. Execute the code (duh), which at minimum uses CPU resources
In this case it really is quite a minimum use of CPU, as React's createElement function doesn't do a whole lot. As far as I know, in a production build it's really just going to do a teeny amount of work to create an object (which looks like {type: Login, props: {}, /* ...some other properties */}). "Evaluating a component" isn't something the user ever does directly. The React runtime is the only thing which ever invokes a component's render method.
> 2. Perform relevant hooks and call lifecycle methods, including those which have side effects (hint: “effect” is in the hook’s name, for this reason): starting timers, loading resources, calling non-React APIs, pretty much anything goes by design, even if you follow all the rules and other best practices.
> 3. Prepare for a new, potentially concurrent, render. This may speculatively interrupt/pause a render in progress which would otherwise complete without interruption. Which may, in turn, cause seemingly unrelated components to be called again, cascading this entire set of potential side effects to them, and so on.
> 4. Call any components in that cascade whether they’re well behaved in your control, or epic shit show dependencies.
It won't do any of these things. React won't try to actually render a component (call its render function) unless an element referring to it ends up being returned by the render function of another component which gets rendered.
I'm sorry, but you're just mistaken about how JSX works.
<Foo /> does not translate into a call to Foo(). It translates into React.createElement(Foo).
And Foo will only be called if the element returned from that call winds up being examined by some renderer like ReactDOM, mounted as a component, and actually rendered.
Here:
function UnrenderedComponent() {
console.log("UnrenderedComponent does not get executed");
React.useEffect(() => {
console.log("this effect never runs")
});
return "This won't get rendered"
}
function RenderedComponent() {
console.log("Rendered Component running")
React.useEffect(() => {
console.log("this effect runs")
});
const x = <UnrenderedComponent/>;
return "This will get rendered";
}
ReactDOM.render(
<RenderedComponent />,
document.getElementById('container')
);
That code unconditionally includes the evaluation of <UnrenderedComponent/> in the context of a render method.
But the UnrenderedComponent() function does not get called, its useEffect call never happens, its effect code never gets executed, because the resulting element doesn't make its way back to ReactDOM to be mounted.
someBoolean && <MyComponent/> is not control flow, it's a simple expression that conditionally evaluates to an instruction to either not render anything, or to mount and render a MyComponent. It's cheap and side effect free.
My comment about 'assuming well behaved React code' was more directed at the fact that technically you can plug in a custom JSX pragma so you might not just be handing it to React.createElement, in which case all bets are off for what happens. But React doesn't call render methods for components unless they're mounted.
> I'm sorry, but you're just mistaken about how JSX works.
No, I’m not. JSX doesn’t do anything. But it’s an expression and evaluated according to the rules of JS expressions. You might expect React or whatever to evaluate it one way today, and it can be evaluated another way tomorrow. React.createElement isn’t the API you’re using, neither is react/jsx-runtime. You’re using an expression with some special angle brackets and curly braces. You have no idea if your component is being called, but semantically you have every reason to believe it is and will be.
Bool && otherExpression is control flow because it’s specifically defined as an expression in the host language. Anything else is assuming compiler magic you were never promised.
You said that “evaluating a component when you don’t intend to use it” will do a bunch of things which React manifestly doesn’t do. I shared a link to some code you can run that demonstrates this. The question of whether or not some code gets executed is, as you said, knowable.
But you are, I think, retreating to a position that, because the JavaScript && operator shortcircuits (ie does not evaluate the second operand of the first operand is truthy) it is always a control flow - a point which I can sort of agree with, but which I think is just a weak position, since it doesn’t help us make decisions about what code is good or bad.
Because if we back up to the top of the thread the point being raised was that use of && for conditionals in react feels like using && to perform control flow, and that is idiomatically bad JS.
But you can’t have it both ways - if using && for control flow is bad and && is always a control flow operation then every use of it is bad. But if there are some times where && use is good, then there must be cases where using &&, even though it shortcircuits, does not count as using it for evil control flow.
But even in simple idiomatic JavaScript we rely on && shortcircuiting for good and valuable purposes like nullsafety:
if (person !== null && person.name === ‘foo’)
So is that using && for control flow, which is bad?
What about here?
return person && person.name;
Or
return person && `Name: ${person.name}`;
.. or even
return person && <Person name={person.name}/>
(Notice what I’m guarding against here is not against incorrect evaluation of the Person control - I’m guarding against incorrect evaluation of one of its parameters)
My position is, && shortcircuiting is a feature and you can use it as such, but if your code relies in a nonobvious way on the shortcircuiting behavior for correctness, then you are using && as a control flow tool, and you have made it brittle, harder to refactor, and harder to parse.
But if your code would still be correct even if JavaScript did not guarantee that it would not evaluate the second parameter if the first was truthy, then you’re not even relying on shortcircuiting, and using && is clean and valid.
And since JSX literals in react are really just shorthand for a special kind of object literal, and in general therefore it doesn’t matter if they are evaluated or not, I take the position that using them within JS && expressions is legitimate, and not an abuse of && to perform complex control flow.
> But you are, I think, retreating to a position that, because the JavaScript && operator shortcircuits (ie does not evaluate the second operand of the first operand is truthy) it is always a control flow
I’m not retreating to anything. This was my only point and I explicitly said so.
> a point which I can sort of agree with, but which I think is just a weak position, since it doesn’t help us make decisions about what code is good or bad.
I’m not trying to change the world with the point. Just to establish the basic fact.
> It doesn’t matter where the expression is placed, it’s the same if you assign it to a variable
If you separate the concern of the condition definition from the conditional rendering, by pulling the conditional’s definition into a variable, you do get enhanced portability, though.
Much easier to port between languages and frameworks if your conditional definition can be copy-and-pasted out without having to mess with untwining the previous developers expression statements.
I’m not sure whether I agree or disagree with this. But I do want to clarify that my intent was not to argue for or against conditional expressions in any code position, only to address the factual question of whether they constitute control flow.
Nonetheless, better for the industry if we start to embrace more uniformity in our idioms.
You can practically copy-and-paste between JavaScript and C# these days, with some trivial text replacement tweaks, if you are careful with your idioms.
Yes Dart with Flutter and swift UI both had to add language features just to support if and loop expressions. But people writing immediate mode guis have been doing the exact same thing using standard language features for years. There's no reason you can't mix React-ive style components with an immediate style API. I do this in a GUI library I've written in D. The downside is you get slightly less type safety, but I'm happy to pay that cost
> swift UI both had to add language features just to support if and loop expressions
The special syntax for loops in SwiftUI is important, because plain-old loops are always eager. For example, if you were building building a list using a loop, it would iterate over every item up-front to generate the list's body. With the `ForEach` struct, on the other hand, you provide the block to create each item, and it can be invoked lazily as the content will appear.
You could very easily implement that pattern yourself in a react app as a few HoCs. So far I think the react project has done a pretty decent job at limiting the surface area of the API. This seems a bit high level.
Solid actually uses them for static analysis which ~isn’t otherwise possible in JSX, which has to be an expression. It would be great if for/if were expressions in JS, but they’re not.
… and I suppose a case could even be made for the granularity of this superfluous <GridItem /> component… but regardless this is just a minor annoyance that comes up from time to time.
Personally I hate implied casting of booleans (and implied casting generally), and go out of my way to make boolean checks explicit whenever I touch the code. These kinds of shortcuts made some sense when minification was commonly a manual thing. But now, I want code to be as verbose as necessary for it to be unambiguous.
Not always worth it when you're just trying to do some conditional logic next to the code/components that it's related to.
Ideally we have the tools to decide when to add indirection ourselves instead being forced to do it to deal with complexity. You could also see this in callback-hell when we'd flatten callback trees with indirection—the tree looked flatter in the editor but we just moved code around. async/await gave us the tools to decide when we actually wanted it.
When logic gets more complicated than a single ternary (e.g., handling pending/error/result state), I've found that a `useMemo` gives me the flexibility I need. It also keeps the messy logic out of the final JSX, which tends to make it read better.
I've personally rarely even needed to useMemo, I just write a function that returns the JSX I need and { myFunction() } in the functional component's return (given React).
If you're using Typescript that can get a bit annoying, since you'll also need to pass in whatever props necessary for the function to handle the logic + type them. Makes it nice and easy to test in isolation though.
Typically the function has access to whatever props or state it needs because it's in the scope of the component. It's not pure, but it's rare that I need to verbosely pass props into the function call and then accept them in myFunction. I don't typically test these functions in isolation as the component itself is pure and I test the output of the component as a whole instead.
Nested ternaries can by structured to look a look like `if/else` if you put the `?/:` in the right spots.
It's a balance. Extracting logic into new components (and often new files!) isn't exactly ergonomic either.
I've found it's better to decrease readability if it makes understanding the logic more straight-forward (i.e. not hunting through several files to exhaust all possible outputs).
This involves people writing giant blocks of JSX that are hard to read. Especially with && all over the place. Just split up your component into smaller components with max one conditional each.
Related, can anyone tell me the origins of the bizarre react practice of one file per component? I'm assume some mega corp told people it was "best practice", but the end result is way too many files with hard to read components.
The ideas described here are just tools and the article contains plenty of simple and readable examples of the tools being used well. If you end up using them to create giant unreadable blocks of JSX then you're using the tools wrong, but that doesn't make the tools themselves an anti-pattern. The anti-pattern there is "giant unreadable blocks of code".
I actually really like the nested ternaries. TypeScript understand them very well and you don't have to extract your code outside the render to start using if/else structures. They also format quite well.
I like ternary conditionals, nested or otherwise. But I also like reduce. And regular expressions. And I know a lot of other devs hate some or all of them. So I tend to use them sparingly when working with a team… although I do see more ternaries in idiomatic code these days, generally as devs embrace a more FP style.
This is usually a good indicator that your components may be doing too much. If your component’s JSX specifically is hard to read, you probably have multiple components implemented within one.