Suppose I’m writing a useFetch
hook and want to encode the result is returns so that it makes impossible states irrepresentable. Something like:
type t('d, 'e) =
| Fetching
| Complete(result('d, [> fetchError] as 'e));
| Refetching(result('d, [> fetchError] as 'e));
So then the consumer has to go:
[@react.component]
let make = () => {
UseFetch.(
useFetch(
"https://api.github.com/search/repositories?q=language:reason&sort=stars&order=desc",
)
->(
fun
| Fetching
| Refetching(_) => ReasonReact.string("Loading...")
| Complete(Ok(({items}: GhRepo.t))) =>
<ul>
{Belt.Array.map(items, ({fullName, htmlUrl}: GhRepo.repo) =>
<li key=fullName>
<a href=htmlUrl> {ReasonReact.string(fullName)} </a>
</li>
)
->React.array}
</ul>
| Complete(Error(`FetchError(_))) =>
<div> {ReasonReact.string("Fetch error!")} </div>
)
);
};
Or something like that, depending on how you handle errors/refetches in your app. Sure, you can get closer to the good old {loading, data, error}
by providing some helpers, but that’s more code, and anyway, is the whole thing even worth it in cases like this?
I mean, arguably the bigger pro of sum types and exhaustive matches is that they get your back during refactoring: you add or remove a variant and than compiler doesn’t stop yelling until you fix every match expression you have. But in case of a well-defined and ubiquitously-used component/hook that queries data, what are the chances that the variant will ever change?
And even if having both some data and an error is an impossible state, should it concern a consumer? Because if, say, UseFetch
provides some helpers like isLoading
and getError
, and the implementation is wrong, the consumer can technically end up with some impossible UI too, only it’ll take more code.
So I kinda see the point of using that or similar variant type internally, but what do you guys think of it as of an external API? Isn’t it overcomplicating things?