Eliminating Illegal State in ReScript
The next thing I’d like to share is how ReScript helps in making illegal states unrepresentable in our apps.
Here’s the common pattern of shaping a state in JS:
type State = {|
loading: boolean,
data: Data | null,
error: Error | null,
|};
What’s wrong with this? Let’s see how it can be handled in UI:
render = () =>
<div>
{this.state.loading && <Spinner />}
{!this.state.loading &&
!this.state.error &&
<div>{this.state.data.foo}</div>
}
{this.state.error && <div>{this.state.error.message}</div>}
</div>
It can be improved a bit by re-shaping this state:
type State = {|
status: "loading" | "ready" | "error",
data: Data | null,
error: Error | null,
|};
render = () => {
switch (this.state.status) {
case "loading":
return <Spinner />;
case "ready":
return <div>{this.state.data.foo}</div>;
case "error":
return <div>{this.state.error.message}</div>;
default:
throw new Error("¯\_(ツ)_/¯");
}
}
However, the main issue remains: conditional dependencies between state properties. When loading === false
(or status === "ready"
), it implicitly assumes that data
is not null
. No guarantees, though. Opened doors into an illegal state.
When you use Flow or TypeScript, these tools warn you that data
might be null
, and you have to add all these annoying checks to calm the type system down:
case "ready":
if (!this.state.data) {
throw new Error("Uh oh no data");
}
return <div>{this.state.data.foo}</div>;
I look at the code above, and I’m just sad.
The light
First of all, let me introduce you to the thing called variant
.
type status =
| Loading
| Ready
| Error
This type is similar to an enum
in TS except its parts are not strings (nor any other data type you are familiar with from JS). These things are called tags
(or constructors
).
Now the magic moment: every tag
can hold its own payload!
type status<'data, 'error> =
| Loading
| Ready('data)
| Error('error)
It means in the provided example, it’s impossible to access the 'data
unless the status is Ready
or access the 'error
unless the status is Error
.
If you’re not there yet, here’s the snippet (mixing ReScript & JS just for clarity’s sake!):
type Status<'data, 'error> =
| Loading
| Ready('data)
| Error('error);
type State = {status: Status};
class Foo extends React.Component {
state = {status: Loading};
componentDidMount = () => {
api.getData()
.then(data => this.setState({status: Ready(data)}))
.catch(error => this.setState({status: Error(error)}));
};
render = ({state}) => (
<Layout>
{
switch (state.status) {
| Loading => <Spinner />
// data is available only when the status is Ready
| Ready(data) => <div>{data.foo}</div>
// error is available only when the status is Error
| Error(error) => <div>{error.message}</div>
}
}
</Layout>
);
}
How beautiful is that! There’s no way to get into an illegal state in this component. Everything is type-safe. Combine it with “everything is an expression” in ReScript, and you can use pattern matching at any point of your JSX render tree (spot switch
as a child of <Layout />
).
You know what to do.