I’ve just finished moving Isolate to ReasonML and React. Today’s post is about that.
Now, I’m no frontend dev. I spend my time at work thinking about systems and infrastructure, migrating things, building incremental value while working towards some far off vision.
Then I come home and work on Isolate, an app I’ve made for myself, to managing art reference and inspiration. Honestly, I just want to have it, not to build it. None of this thinking hard, following best practices, whatever. I’ve been playing it fast and loose with global variables, redundant code, unclear interfaces. And what do you know I’ve found myself in the odd position of having incurred technical debt in a side project.
One day, when all the problems are solved and there’s nothing left to do, I’ll move this to QT. But for now, I just wanted to clean things up a bit and move back to adding some features.
TL;DR
React: Great!
Reason: Great!
React
Imposes code structure
As I worked, I broke up code into logically grouped modules. This is nice for library-esque modules
like path
, but did little to organize the application as a whole. Largely it ended up with code
reaching across interfaces and no real abstraction.
React imposed some much needed code structure. In React, you define components, essentially functions that define how to construct some HTML. An application is built by composing these components in a hierarchy: a component can render html, or nest calls to other components.
React automagically handles re-drawing components when the arguments (“props”) of the components change, offloading the significant headache of managing how to subscribe to changes, or updating dependents.
This is also really nice because it’s a lot easier to reason about components in isolation. All the dependencies are described as props (and typed with Reason!).
Modal.re is a good example, here
it only depends on State.Modal.state
and an action to update the modal state State.Modal.action =>
unit
and then defines how to render the HTML given the current state. This is compared to
before
where the modal module described how to mutate the modal DOM object. You can probably have written a
lot better javascript than that without React, but it really pushes down a good structure.
State
I haven’t found a good way to have sibling components affect each other (probably good I haven’t), and so React forced me to accept what is shared state. E.g. if the header contents depends on if there is an active search, that’s not search-local state, it’s either header-local or global. Because Isolate has relatively little state, I’ve just been pushing it up to a global state if it’s shared.
ReasonReact includes a mini Redux model, where a stateful component includes a function (action,
state) => state
that pattern matches on the possible actions and defines how to update the state.
Unfortunately I’m still struggling to find a good way of composing the reducer function. ReasonReact encourages putting a lot into it, but my main reducer is getting a bit excessive.
Declarative
Previously, the UI would be imperatively toggled, which scaled abysmally. Clicking on an image would
trigger adding display: block
on the modal container, updating a bunch of fields in the process.
New UI elements would each mutate each other’s DOM element, and the DOM became this tangled
implicit, global state.
In React, you describe what should be rendered given a component’s props, not how to get there. With JSX (inlining HTML in Javascript), this is incredibly convenient.
All together, previously searching would hide/show elements in the header like:
function search(term) {
...
hide(ui.pwd)
hide(ui.dirs)
show(document.querySelector('#search-controls'))
}
document.querySelector('#search-controls a').onclick =
e => {
hide(document.querySelector('#search-controls'))
show(ui.pwd)
show(ui.dirs)
...
}
Notably, every possible path to updating state would have to update the DOM. Now, instead, the header describes how to be rendered:
render: self => {
...
<header>
<Search ... />
(if (!self.state.search)) {
<div>
<h3> (ReasonReact.string(pwd)) </h3>
<Directories ... />
</div>;
} else {
ReasonReact.null;
})
</header>;
...
}
ReasonML
Reason (OCaml) feels like a really tight, consistent language. The docs are great, and focus on motivating the switching from Javascript.
Having types is wonderful; adding types for relative and absolute filepaths saved me so many bugs. Refactoring is much saner when you repeatedly compile and fix errors until the build succeeds.
Reason does a really good job with Javascript interop. It’s easy to inject raw Javascript, and wrap it in types. The output of Bucklescript (Reason compiler) is just a (surprisingly readable) transpiled javascript file, so it’s easy to include in a Javascript file for the other way around.
Most of Isolate is the UI, so I haven’t gone too deep into Reason. To keep it pragmatic, it says a lot that it added value without getting in my way. If that’s not convincing, I will say I look forward to using it again.
Troubles
Briefly, the pains I faced were:
- ReasonML’s compiler errors are very vague. These two errors comprise almost all the errors I ever saw:
File "/Users/seena/Developer/isolate/src/Main.re", line 14, characters 0-1:
Error: 3724: <UNKNOWN SYNTAX ERROR>
This has type:
string
But somewhere wanted:
Isolate.Path.base (defined as Isolate.Path.base)
- Mutating Javascript value from Reason.
I couldn’t for the life of me find a way to set document.onclick
to a function defined in Reason
from Reason.