Slaying a UI Antipattern with Flow

TL;DR You don’t really need a different language (Elm) or an adt library (folktale, daggy), often you can just use vanilla JavaScript and a good type checker.

The problem

This problem is presented in Slaying a UI Antipattern in Fantasyland by Stefan Oestreicher which in turn is based on How Elm Slays a UI Antipattern by Kris Jenkins.

The problem they present is a very common one. You are loading a list of things but instead of showing a loading indicator you just see zero items. In JavaScript your data model may look like this

{ loading: true, items: [] }

But of course it’s easy to forget to check the loading flag. What about using null in order to represent the “not loaded” case? Both Stefan and Kris wisely discourage it

Long experience will have taught you that setting a property to null may be correct, but it’s just asking for runtime exceptions

Fortunately, this concern evaporates when using Flow and its maybe types

type Model = {
things: ?Array<Thing>
};

Now if you try to use an instance of this model incorrectly

const SomeView = ({ things }: Model) => {
return <div>
{ things.map(thing => { ... }) }
</div>
}

Flow will complain

{model.things.map(thing => {})}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `map`. Method cannot be called on possibly null value
{model.things.map(thing => {})}
^^^^^^^^^^^^ null

If you want to be more explicit, just define a type alias

type Maybe<A> = ?A;
type Model = {
things: Maybe<Array<Thing>>
};

However we can go even further, as Kris says “we can be much more sophisticated”.

The solution

HTTP requests have one of four states

  • we haven’t asked yet
  • we’ve asked, but we haven’t got a response yet
  • we got a response, but it was an error
  • we got a response, and it was the data we wanted

With Flow we can easily define a type that represents these four states

type RemoteData<E, D>
= { type: 'NotAsked' }
| { type: 'Loading' }
| { type: 'Failure', error: E }
| { type: 'Success', data: D };
type Model = {
things: RemoteData<HttpError, Array<Thing>>
};

As Kris observes

The nice thing about this data model is, the type checker will now force you to write the correct UI code. It will keep track of the possibility of “things not loaded” and errors, and force you to handle them all in the UI.
// will raise: property `data`. Property not found in object type
const SomeView = ({ things }: Model) => {
return <div>
{ things.data.map(thing => {}) }
</div>
}

Flow will force you to handle all cases

const SomeView = ({ things }: Model) => {
if (things.type === 'NotAsked') {
return <div>Please press the button to load the things</div>
}
else if (things.type === 'Loading') {
return <div>Loading things...</div>
}
else if (things.type === 'Failure') {
return <div>An error has occurred { things.error }</div>
}
return <div>
{ things.data.map(thing => { ... }) }
</div>
}

Or, using a more functional style, let’s define a fold function that can be re-utilised in more use cases

function fold<R>(
notAsked: () => R,
loading: () => R,
failure: (error: HttpError) => R,
success: (data: Array<Thing>) => R
): (model: Model) => R {
return ({ things }) => {
return things.type === 'NotAsked' ? notAsked() :
things.type === 'Loading' ? loading() :
things.type === 'Failure' ? failure(things.error) :
success(things.data) }
}

Now SomeView can be defined as

const SomeView = fold(
() => <div>Please press the button to load the things</div>,
() => <div>Loading things...</div>,
(error) => <div>An error has occurred { error }</div>,
(data) => <div>{ data.map(thing => {}) }</div>
)

Note. Elm, folktale and daggy are definitely useful and they have their specific use cases, this post just shows you that you can go a long way with vanilla JavaScript and Flow.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.