The following pattern gets repeated in my React app codebase quite a bit:
const {items, loading} = this.props
const elem = loading
? <Spinner />
: items.length
? <ListComponent />
: <NoResults />
While this is certainly cleaner than nesting actual if/else
clauses, I'm trying to embrace more elegant and functional patterns. I've read about using something like an Either
monad, but all of my efforts towards that have ended up looking more verbose, and less reusable (this pseudo-code probably doesn't work, given that I'm trying to remember previous attempts):
import {either, F, isEmpty, prop} from 'ramda'
const isLoading = prop('loading')
const renderLoading = (props) => isLoading(props) ? <Spinner /> : false
const loadingOrOther = either(renderLoading, F)
const renderItems = (props) => isEmpty(props.items) ? <NoResults /> : <ListComponent />
const renderElem = either(loadingOrOther, renderItems)
const elems = renderElem(props)
What pattern can I use that would be more DRY/reusable?
Thanks!
While this is certainly cleaner than nesting actual
if/else
clausesrender () { const {items, loading} = this.props return loading ? <Spinner /> : items.length ? <ListComponent items={items} /> : <NoResults /> }
You've posted incomplete code, so I'm filling in some gaps for a more concrete example.
Looking at your code, I find it very difficult to read where conditions are, and where return values are. Conditions are scattered across various lines at various indentation levels – likewise, there is no visual consistency for return values either. In fact it's not apparent that loading
in return loading
is even a condition until you read further into the program to see the ?
. Choosing which component to render in this case is a flat decision, and the structure of your code should reflect that.
Using if/else
produces a very readable example here. There is no nesting and you get to see the various types of components that are returned, neatly placed next to their corresponding return
statement. It's a simple flat decision with a simple, exhaustive case analysis.
I stress the word exhaustive here because it is important that you provide at minimum the if
and else
choice branches for your decision. In your case, we have a third option, so one else if
is employed.
render () {
const {items, loading} = this.props
if (loading)
return <Spinner />
else if (items.length)
return <ListComponent items={items} />
else
return <NoResults />
}
If you look at this code and try to "fix" it because you're thinking "embrace more elegant and functional patterns", you misunderstand "elegant" and "functional".
There is nothing elegant about nested ternary expressions. Functional programming isn't about writing a program with the fewest amount of keystrokes, resulting in programs that are overly terse and difficult to read.
if/else
statements like the one I used are not somehow less "functional" because they involve a different syntax. Sure, they're more verbose than ternary expressions, but they operate precisely as we intend them to and they still allow us to declare functional behaviour – don't let syntax alone coerce you into making foolish decisions about coding style.
I agree it's unfortunate that if
is a statement in JavaScript and not an expression, but that's just what you're given to work with. You're still capable of producing elegant and functional programs with such a constraint.
Remarks
I personally think relying upon truthy values is gross. I would rather write your code as
render () {
const {items, loading} = this.props
if (loading) // most important check
return <Spinner />
else if (items.length === 0) // check of next importance
return <NoResults />
else // otherwise, everything is OK to render normally
return <ListComponent items={items} />
}
This is less likely to swallow errors compared to your code. For example, pretend for a moment that somehow your component had prop values of loading={false} items={null}
– you could argue that your code would gracefully display the NoResults
component; I would argue that it's an error for your component to be in a non-loading state and without items, and my code would produce an error to reflect that: Cannot read property 'length' of null
.
This signals to me that a bigger problem is happening somewhere above the scope of this component – ie this component has either loading=true or some array of items (empty or otherwise); no other combination of props is acceptable.
I think your question isn't really about if statement vs ternaries. I think your perhaps looking for different data structures that allow you to abstract over conditionals in a powerful DRY manner.
There's a few datatypes that can come in handy for abstracting over conditions. You could for example use an Any
, or All
monoid to abstract over related conditions. You could use an Either
, or Maybe
.
You could also look at functions like Ramda's cond
, when
and ifElse
. You've already looked at sum types. These are all powerful and useful strategies in specific contexts.
But in my experience these strategies really shine outside of views. In views we actually want to visualise hierarchies in order to understand how they'll be rendered. So ternaries are a great way to do that.
People can disagree what "functional" means. Some people say functional programming is about purity, or referential transparency; others might say its simply "programming with functions". Different communities have different interpretations.
Because FP means different things to different people I'm going to focus on one particular attribute, declarative code.
Declarative code defines an algorithm or a value in one place and does not alter or mutate in separate pieces imperatively. Declarative code states what something is, instead of imperatively assigning values to a name via different code paths. Your code is currently declarative which is good! Declarative code provides guarantees: e.g. "This function definitely returns because the return
statement is on the first line".
There's this mistaken notion that ternaries are nested, while if statements are flat. It's just a matter of formatting.
return (
condition1
? result1
: condition2
? result2
: condition3
? result3
: otherwise
)
Place the condition on its own line, then nest the response. You can repeat this as many times as you want. The final "else" is indented just like any other result but it has no condition. It scales to as many cases as you'd like. I've seen and written views with many flat ternaries just like this, and I find it easier to follow the code exactly because the paths are not separated.
You could argue if
statements are more readable, but I think again readable means different things to different people. So to unpack that, let's think about what we're emphasising.
When we use ternaries, we are emphasising there is only one possible way for something to be declared or returned. If a function only contains expressions, our code is more likely to read as a formula, as opposed to an implementation of a formula.
When we use if statements we are emphasising individual, separated steps to produce an output. If you'd rather think of your view as separate steps, then if statements make sense. If you'd prefer to see a view as a single entity with divergent representation based on context, then ternaries and declarative code would be better.
So in conclusion, your code is already functional. Readability and legibility is subjective, focus on what you want to emphasise. Do not feel like multiple conditions in an expression is a code smell, its just representative of the complexity of your UI, and the only way to solve that (if it needs to be solved) is to change the design of your UI. UI code is allowed to be complex, and there's no shame in having your code be honest and representative about all it's potential states.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With