react: discriminated unions and semantic composability

2021-09-10

 | 

~3 min read

 | 

446 words

I’ve written in the past about composability and polymorphism in the past and it’s a concept that continues to interest me.

I was speaking with a colleague recently about how we might be able to create more reusable components in React.

Specifically, we wanted to divorce the semantics of the markup (whether the element was a button, an input, or a link) from the appearance of the element. At the same time, we were hunting for solutions that would make the use of these components ergonomic.

Perhaps it will be useful to think about some examples in greater details.

Imagine an app that has the concept of a “card”.

Except card is a suitcase word. Folks pack into whatever they mean. So let’s try to be specific.

In our app, a card lays out information. Sometimes there’s a lot of information in a stack layout. Other times it’s just a single word.

Cards can also be navigable. When they’re clicked, they navigate the user to a new page in the app (and when they do, you want to make sure they take you to the top of the page).

But sometimes they’re not. Sometimes they are used to indicate a selection. When the card is selected, it has a highlighted border.

Cards also have different “flavors”. For simplicity, think of a white card and a red card.

At this point we have:

  1. Layout: Simple | Stack
  2. Behavior: Navigable | Selectable
  3. Flavor: White | Red

If we just opened up the API to the component to handle all these cases, we’d have eight possible states. And the number of permutations here is still relatively small! Not only that, but the different behaviors require totally different information.

This leads to a few undesirable possibilities:

  1. A combinatorial explosion of possible states
  2. Callers of these Cards need to supply a lot of unnecessary details (e.g., the destination for a button, an onClick for a link) or we lose type safety and/or intellisense support by marking everything optional.

Fortunately, we know discriminated unions are quite capable at combatting exploding cardinality. But, how might that work?

My first attempts are documented in this CodeSandbox which is definitely still a work in progress.

Things that I like about this pattern:

  1. It provides a lot of control to the caller
  2. It enables strongly typed / guided composition (i.e., intellisense is available and useful)
  3. The discriminated unions mean that each piece is easy to reason through

Things I don’t love (yet):

  1. It’s rather verbose
  2. It’s hard to tell if it’s worth it. This might be an example of a hasty abstraction in many cases.

Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!