In late 2018 I decided to fork my own library hx to make what I felt were some breaking changes. In my mind, it was a complete change of course: hx had been focused on parsing hiccup into React elements, and had included some utilities for defining components and using hooks as a convenience. Helix, the new library, was to focus on defining components and using hooks in a simple, fast, and idiomatic way to ClojureScript, and eschew hiccup or other special syntax that imposed more runtime cost than I found desirable.
Now in 2021, I am looking towards when I can call helix done. Not because I am sick of it, but because I believe that this leads to the most sustainable path as both a maintainer and user of software. I want to take the learnings from using helix in production over the last year and turn that into the highest quality library that people can use as a foundation for building UIs. Helix’s goal remains the same in 2021 as in 2018: provide a simple, easy to use API for React in ClojureScript.
This blog post serves as a place for me to write down all of my hopes and dreams
for what helix 1.0
would look like. This is not a formal road map per se, but
a snapshot of the things that I would like to add and change in helix before I
call it done. I hope that it generates some discussion, at least.
Of course, there will always be changes and additions to ReactJS that need support. My hope is that those changes will either be opaque from an API perspective, or can be provided by another library (e.g. server-side components).
One of the early mistakes that helix made in it’s API was trying to
automagically handle converting prop keys from Clojure idioms, like :on-click
,
to JS "onClick"
. This was necessary work for the DOM macros in helix.dom
,
and I naively assumed that I could generalize this to handle any “native” React
component that used JS idioms. Hence, I provided the out of the box ability to
pass in any keyword, string or annotate a symbol with metadata {:native true}
,
and the $
macro would do this conversion for you.
As it turns out, idioms are hard to capture as a collection of programmatic rules, and humans do not always apply them consistently. This meant that there were a number of cases where this “native” conversion did the wrong thing, and as a user you were left with mysterious error messages and, once you got to the root of it, a question of how to address it in Helix’s own idioms.
For instance, the style
prop in react-dom
should always be a camelCased JS
object. But in React Native, the style prop can also be a JS array of objects,
or an opaque StyleSheet
type.
Another example is react-three-fiber
which has its own idioms and special
props which should (preferably) be converted to JS native types. Helix’s
“native” conversion doesn’t handle this well at all.
Even the common scenario of using a 3rd party component in a react-dom
application isn’t well served by annotating it with :native
, as many implement
JS idioms inconsistently.
The solution to this problem in helix 1.0
could be to provide specific macros
for creating “native” elements in common platforms, e.g. helix.dom/$d
, and
removing the special handling of keyword, string and :native
annotated
elements passed to helix.core/$
. This way, you always know exactly what $
is
doing: taking your Clojure map as written and shallowly converting it into a JS
object.
If you are using helix.dom
to create DOM elements, e.g. helix.dom/div
and
helix.dom/button
, and using no other “native” components in your code, then
this change would not impact you at all as the helix.dom
element macros would
be implemented using the new platform-specific macro, which would have the same
behavior as the “native” conversion in 0.0.13
.
If you’re using the :native
annotation in your react-dom
application today,
then a future version of helix (before 1.0) would emit a compiler warning that
informs you where it is being used and how to fix it using the new $d
macro.
If you’re using helix with something other than react-dom
, e.g. react-native
or react-three-fiber
, then to solve the compiler warning may involve creating
your own convenience macro, like $rn
and $3f
, to create elements out of
React Native and three-fiber components. My hope is that we can collaborate and
solve this as a community, as the current state is confusing as it is for
newcomers.
This is not a breaking change, but certainly a welcome one. Helix currently allows users to enable feature flags locally in each component in the map of options after the args list of a component:
(defnc my-component
[props]
{:helix/features {:fast-refresh true}})
This allows library and application authors to control which features are used in their code without stepping on each others toes.
The downside to this is that it is quite tedious to write. The solution proposed
by helix’s documentation is to create your own defnc
macro,
but this is onerous for application developers who have already adopted helix
without creating a macro and now want to toggle a specific feature flag for
their application.
In helix 1.0
, you wouldl be able to toggle most feature flags globally at
build time in order to allow this. It would still be suggested that for anyone
authoring a library that depends on helix to create a custom macro if you want
to rely on any feature flags that have runtime impact.
defhook
static analysis and deps inferenceAnother addition: helix currently has a group of macros in the helix.hooks
namespace which paper over some of the short comings of React’s API. Notably,
for hooks like use-memo
and use-callback
, the macros provide dependency
inference so that they can provide React with the values that should cause the
values to be recomputed.
The goal of the defhook
macro is to allow authors of custom hooks to take
advantage of this same static analysis in a light-weight way. For example:
(defhook use-interval
[^:deps dependencies f interval]
(helix.hooks/use-effect
dependencies
(let [interval (js/setInterval f interval)]
#(js/clearInterval interval))))
;; using deps inference
(let [[user-id set-user-id] (helix.hooks/use-state "2")]
(use-interval
:auto-deps
(fn []
(fetch-notifications {:user/id user-id}))
100))
;; is the same as
(let [[user-id set-user-id] (helix.hooks/use-state "2")]
(use-interval
[fetch-notifications user-id]
(fn []
(fetch-notifications {:user/id user-id}))
100))
This could greatly reduce the amount of defensive memoization necessary to ensure that code fires or recomputes only when we want to.
This would be opt-in and would be implemented as code transformation inside of
components defined with helix’s defnc
macro and custom hooks defined with
defhook
.
I would also like to explore alternatives to the explicit :auto-deps
keyword,
either eliding the dependency vector completely or some cuter way of annotating
that dependencies should be inferred.
defhook
The cute and clever wrapping of the body of use-effect
and use-memo
inside a
function for you is less useful than I had hoped. It actively fights against
some attempts at DRY, like:
;; `f` returns the function that should be run by the memo
(hooks/use-memo
[foo bar]
(f foo bar))
;; instead we must lexically bind the value and call it in the body
(let [fn-to-run (f foo bar)]
(hooks/use-memo
[foo bar]
(fn-to-run)))
One can see as well that this prevents static analysis of the dependent values
used by fn-to-run
.
The 1.0
solution would be to define helix’s fundamental hooks without macros,
using defhook
- or at least, requiring that you pass in a 0-arity function
instead of wrapping the body in one automatically for you - and moving them into
helix.core
. So the above example could be rewritten as:
(helix.core/use-memo
[foo bar]
(f foo bar))
;; normal usage without the factory function `f`
(helix.core/use-memo
[foo bar]
#(+ foo bar))
An earlier version can introduce these new core hooks and deprecate
helix.hooks
so that helix 1.0
could remove helix.hooks
.
The most joyful part of working on helix has been how many people have contributed to it by adding features, fixing bugs, creating documentation and discussing various tricks and tradeoffs. It has been adopted by a number of professional ClojureScript groups as the fundamental library for building UI, including my own employer. The library is better and I am wiser because of it.
Let me know what you think of these ideas! Reach out to me on #helix in Clojurians slack or email me.