5

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!

EyasSH
  • 3,679
  • 22
  • 36
Kevin Whitaker
  • 12,435
  • 12
  • 51
  • 89
  • 1
    I wouldn't use `Either`, which rather represents short circuiting. This seems to be a use case for a sum type. Unfortunately, Javascript doesn't ship with sum types and pattern matching. –  Mar 23 '17 at 12:54
  • Early optimizations will only make it worse – ffflabs Mar 23 '17 at 13:02
  • 3
    DRY? It already is. Reusable? Put it in a function. – Bergi Mar 23 '17 at 14:41
  • 1
    I tried it with sum types and it got verbose and hard to read. Please note that `if` expressions (or the conditional operator in Javascript) are not harmful, but `if` statements are, because the latter rely on side effects. –  Mar 23 '17 at 15:14
  • 2
    `if` statements are not harmful and do not rely upon side effects. There is nothing wrong with `if (cond) return x; else if (cond2) return y; else return z;` – Mulan Mar 23 '17 at 18:55
  • @naomik `return` would be considered a side effect here :-) For absolute purity, you'd only use expressions and no statements. And arrow functions with concise bodies… – Bergi Mar 23 '17 at 19:31
  • 1
    @Bergi this is why I talk about functional *behaviour* separate from functional *syntax* in my answer – if a function's behaviour is examine up to two conditions and return 1 of 3 elements, I would not treat `return` as a side effect here, just because it could be *syntactically* considered a side effect. – the *behaviour* is get some element based on conditions and that is the *only* effect of this function - therefore it remains functionally pure, despite syntactic pedantry. – Mulan Mar 23 '17 at 19:59
  • @Kevin I have been working on sum types and pattern matching for quite a while now and updated my answer to share my findings, in case you are interested. –  May 13 '17 at 22:28

5 Answers5

5

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.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    "*it's unfortunate that `if` is a statement*" - that's what we have ternary operators for :-) The rest is "just" localisation of keywords and formatting. – Bergi Mar 23 '17 at 19:33
  • 2
    Ha, I was actually hesitant to put that comment into my answer, because of how underpowered I feel ternary is JavaScript. I was particularly thinking of something like Ruby's `if` expression that allows for multiple statements/expressions but still produces a "return" value – [repl.it example](https://repl.it/GbSu) – Mulan Mar 23 '17 at 19:39
  • 1
    "_in this case is a flat decision_" - interestingly, when I implemented the corresponding sum type I ended up with two sum types, where one (representing a component that may be empty) is nested within the other (representing a component that may be deferred). –  Mar 23 '17 at 19:47
  • 2
    @naomik `true ? (x = 1, y = 2, x + y) : (x = 3, y = 4, x - y)` - the only thing that JS lacks are `let` expressions to properly declare those local variables. :-P – Bergi Mar 23 '17 at 19:52
  • 1
    @Bergi yes I'm aware of `(expr1, expr2, ...exprN)` syntax, but it is not a true analog because (as you mentioned) `var/let/const` cannot be written in expressions. Maybe my actual despair lies with `let` et al being a statement in JavaScript instead of getting something like `let x = 5 in x + 5 // => 10` – Mulan Mar 23 '17 at 20:02
  • 1
    Ok, I follow your post, and understand the points you make. In fact, I agree with them; but in reading your post, I realize that I asked the wrong question. I'm going to mark this as answered by your response, and see if I can formulate a better, more precise question. Thanks for the discussion :) – Kevin Whitaker Mar 23 '17 at 20:10
  • 1
    @Bergi, for what it's worth, I think *all* statements in JavaScript are a real bear to deal with. I'd much prefer that all programs could be expressed using *only* expressions. But due to limitations of JavaScript's syntax, we either compromise by using some statements, or we end up with code that looks like lambda calculus (totally unreadable). I don't think use of statements in JavaScript prevents a program from being expressed in a functional way, so I bend on some functional *syntax* rules but keep focus on expressing programs with functional *behaviour*. – Mulan Mar 23 '17 at 20:13
  • 1
    @KevinWhitaker it's a tough question because there's heaps of pedantry around "what is *functional* programming" and what that looks like in JavaScript. I thank Bergi and ftor for giving me an opportunity to reply to some comments. It sounds like the discussion was helpful and I'm happy to be part of it. – Mulan Mar 23 '17 at 20:17
  • 1
    @naomik Yeah, `((x, y) => x+y)(1,2)` looks weird :-) There's a [proposal for `do` expressions though](https://gist.github.com/dherman/1c97dfb25179fa34a41b5fff040f9879) – Bergi Mar 23 '17 at 20:46
  • 4
    I love this discussion. When I started Ramda a few years ago, it was just a learning exercise. Somehow it took off. The best part has been the number of smart people who've come to help out. While this answer ended up having nothing to do with Ramda, the thoughtfulness of the comments is fantastic. – Scott Sauyet Mar 24 '17 at 01:08
  • @naomik: I have one serious objection to your answer: you bury an important change in your refactoring. You start with an expression and convert it to a function in order to allow the `returns` to work. I don't know the React (I assume) environment well enough to know if this `render` wrapper should just be obvious, but it's certainly not clear from the question that one could simply return the result of that expression. But even if it should be obvious, that still begs the question, "what if you can't?" Does the only way to answer such questions involve the extraction of functions? – Scott Sauyet Mar 24 '17 at 01:15
  • @ScottSauyet I value the discussion as well. I changed his `const` *statement* to an `if` statement. I made the assumption that this was the body of a React component's `render` method, but if it's not, it is common to extract this kind of thing into its own method. – Mulan Mar 24 '17 at 02:07
  • @naomik: Such extraction is common, and usually helpful. My point is just that this is a fairly substantial change, and when you are not in position to abstract like this, then you end up with setting the values of a nasty temporary variable instead of the reasonably clean `return`s. Still, any solution I can come up with, would probably involve the same extraction, so I don't know that there's anything better. But I have mixed feelings about the relative worth of the extraction versus the conditional operator, even a nested one. – Scott Sauyet Mar 24 '17 at 02:29
  • Pretty sure the second-to-last paragraph here is a huge misreading of OP's code. Your example and his both access `items.length` so both would throw the same error if `items` were `null`. Explicitly comparing `items.length` to 0 may improve readability, but it doesn't provide any runtime benefit. – JLRishe Nov 18 '17 at 13:49
5

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.

James Forbes
  • 1,487
  • 12
  • 15
  • 1
    The answer is always contextual. In my experience view code changes in structure more often than models, or other types of data transformation. Abstracting over the structure when a structure is volatile just leads to time wasted. But if the conditional is isolated you can localize the condition. – James Forbes Mar 25 '17 at 02:43
  • An example: some virtual doms (e.g. mithril ) ignore boolean's, undefined and nulls when generating nodes. You could have a `loading(state)` view that returns the spinner or returns null/false. And you could have a separate view for rendering items that abstracts over whether or not the items list is empty. Your view can then rely on short circuiting: `return loading(state) || items(state)`. In this case changes in representation are unlikely to affect our abstractions because the conditions are local and isolated. – James Forbes Mar 25 '17 at 02:46
  • If you're uncomfortable with using truthiness, you could formalize this as `return Any(loading(state)).concat( Any(items(state)).fold() ` which could pick the first truthy view and return that. – James Forbes Mar 25 '17 at 02:48
4

Sum types and pattern matching

You can use sum types and pattern matching to avoid if/else statements. Since Javascript doesn't include these features you have to implement them by yourself:

const match = (...patterns) => (...cons) => o => {
  const aux = (r, i) => r !== null ? cons[i](r)
   : i + 1 in patterns ? aux(patterns[i + 1](o), i + 1)
   : null;

  return aux(patterns[0](o), 0);
};

match takes a bunch of pattern functions and constructors and data. Each pattern function is tested against the data unless one matches. The corresponding constructor is than invoked with the result of the successful pattern function and returns the final result.

In order for match to recognize whether a pattern match was unsuccessful, the patterns must implement a simple protocol: Whenever a pattern does not match, the function must return null. If the pattern matches but the corresponding constructor is a nullary constructor, it must simply return an empty Object. Here is the pattern function for the spinner case:

({loading}) => loading ? {} : null

Since we use destructuring assignment to mimic pattern matching, we have to wrap each pattern function in a try/catch block to avoid uncaught errors during destructuring. Hence we call pattern functions not directly but with a special applicator:

const tryPattern = f => x => {
  try {
    return f(x);
  } catch (_) {
    return null;
  }
};

Finally, here is a constructor for the spinner case. It takes no arguments and returns a JSX spinner element:

const Spinner = () => <Spinner />;

Let's put it all together to see how it works:

// main function

const match = (...patterns) => (...cons) => x => {
  const aux = (r, i) => r !== null ? cons[i](r)
   : i + 1 in patterns ? aux(patterns[i + 1](x), i + 1)
   : null;

  return aux(patterns[0](x), 0);
};

// applicator to avoid uncaught errors during destructuring

const tryPattern = f => x => {
  try {
    return f(x);
  } catch (_) {
    return null;
  }
};

// constructors

const Spinner = () => "<Spinner />";
const NoResult = () => "<NoResult />";
const ListComponent = items => "<ListComponent items={items} />";

// sum type

const List = match(
  tryPattern(({loading}) => loading ? {} : null),
  tryPattern(({items: {length}}) => length === 0 ? {} : null),
  tryPattern(({items}) => items !== undefined ? items : null)
);

// mock data

props1 = {loading: true, items: []};
props2 = {loading: false, items: []};
props3 = {loading: false, items: ["<Item />", "<Item />", "<Item />"]};

// run...

console.log(
  List(Spinner, NoResult, ListComponent) (props1) // <Spinner />
);

console.log(
  List(Spinner, NoResult, ListComponent) (props2) // <NoResult />
);

console.log(
  List(Spinner, NoResult, ListComponent) (props3) // <ListComponent />
);

Now we have a List sum type with three possible constructors: Spinner, NoResult and ListComponent. The input (props) determine which constructor is finally used.

If List(Spinner, NoResult, ListComponent) is still too laborious for you and you don't want to list the individual states of your List explicitly, you can pass the constructors already during the sum type definition:

const List = match(
  tryPattern(({loading}) => loading ? {} : null),
  tryPattern(({items: {length}}) => length === 0 ? {} : null),
  tryPattern(({items}) => items)
) (
  Spinner,
  NoResult,
  ListComponent
);

Now you can simply call List(props1) etc. in a quite DRY manner.

match silently returns null if no pattern matches. If you want a guarantee that at least one pattern matches successfully, you can also throw an error.

  • This is an interesting pattern. I believe I've got most of the helper functions already with Ramda. I'll play with it and see how it looks. Thanks! – Kevin Whitaker Mar 27 '17 at 12:06
2

Since Ramda has an ifElse function, you can use that to write your condition in a reusable, pointfree style.

Runnable example (using strings instead of <Tags> so it can be run as a stack snippet).

const { compose, ifElse, always, prop, isEmpty } = R;

const renderItems = ifElse(isEmpty, always('noResults'), always('listComponent'));

const renderProps = ifElse(
    prop('loading'), 
    always('spinner'), 
    compose(renderItems, prop('items'))
);

// usage: const elem = renderProps(this.props);

// test
console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] }));
console.log(renderProps({ loading: false, items: [] }));
console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] }));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

Of course, another option is to use arrow functions and the conditional operator to split your condition into two functions. Like the above example, this gives you a reusable renderItems function:

const renderItems = list => list.length ? 'listComponent' : 'noResults'; 
const renderProps = props => props.loading ? 'spinner' : renderItems(props.items);

// usage: const elem = renderProps(this.props);

// test
console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] }));
console.log(renderProps({ loading: false, items: [] }));
console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] }));
JLRishe
  • 99,490
  • 19
  • 131
  • 169
0

You don't have to install extra packages for this:

content() {
  const {items, loading} = this.props
  if (loading) {
    return <Spinner />;
  }
  return items.length ? <ListComponent /> : <NoResult />;
}

render() {
  return this.content();
}
hjrshng
  • 1,675
  • 3
  • 17
  • 30