Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript: How can I replace nested if/else with a more functional pattern?

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!

like image 570
Kevin Whitaker Avatar asked Mar 23 '17 12:03

Kevin Whitaker


2 Answers

While this is certainly cleaner than nesting actual if/else clauses

render () {
  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.

like image 110
Mulan Avatar answered Oct 11 '22 15:10

Mulan


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.

like image 25
James Forbes Avatar answered Oct 11 '22 14:10

James Forbes