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.