-
-
Save lilactown/e93a1a0ab25d40df006d77f405c1e535 to your computer and use it in GitHub Desktop.
(ns helix-example | |
(:require | |
[helix.core :refer [defnc $ <>]] | |
[helix.hooks :as hooks] | |
[helix.dom :as d] | |
["react-dom" :as rdom])) | |
(defnc Greeting | |
"A component which greets a user. The user can double click on their name to edit it." | |
[{:keys [name on-name-change]}] | |
(let [[editing? set-editing?] (hooks/use-state false) | |
input-ref (hooks/use-ref nil) | |
focus-input #(when-let [current (.-current input-ref)] | |
(.focus current))] | |
(hooks/use-layout-effect | |
:auto-deps ;; automatically infer deps array from body; stand in for `[editing?]` | |
(when editing? | |
(focus-input))) | |
(d/div | |
"Hello, " (if editing? | |
(d/input {:ref input-ref | |
:on-change #(on-name-change (.. % -target -value)) | |
:value name | |
:on-blur #(set-editing? false)}) | |
(d/strong {:on-double-click #(set-editing? true)} name) | |
"!"))) | |
(defnc App [] | |
(let [[state set-state] (hooks/use-state {:name "Helix User"}) | |
;; annotate with `:callback` metadata to automatically wrap in | |
;; `use-callback` and infer dependencies from local context | |
on-name-change ^:callback #(set-name assoc :name %) | |
;; annotate with `:memo` metadata to wrap in `use-memo` and infer deps as well | |
name ^:memo (:name state)] | |
(<> (d/h1 "Welcome!") | |
($ Greeting {:name name})))) | |
(rdom/render ($ App) (js/document.getElementById "app")) |
Cool, thanks for the explanation. Another question:
Is there a story on interop where JS components will give you already-in-JS prop maps, which you need to use from CLJS? My current solution is to wrap them in bean
so that they can be passed to a an hx
component, perhaps doing a merge
with some other props too. Would be helpful if $
recognised plain JS maps and didn't try to convert them. A function for prop merging could also help.
An example of how this looks like from a real component, integrating react-beautiful-dnd -- which brings in Draggable:
(defnc DraggableSelectableRow [{:keys [id index value cells-component]}]
(let [selected? (is-selected? id)]
[Draggable {:draggableId id
:key id
:index index}
(fn [provided snapshot]
[:tr (merge {:ref (.-innerRef provided)
:class [(when selected? "table--selected")
(when (.-isDragging snapshot) "table-row-dragging")]}
(bean (.-draggableProps provided)))
[table/SelectionCell {:id id :is-selected selected?}]
[cells-component {:value value}]
[:td (merge {:class "table-row--draggable"}
(bean (.-dragHandleProps provided)))
[:span {:class "icon move"} [:i]]]])]))
Dynamic props are tough.
What I've done so far is introduce an idea of spread props in the $
macro (this includes helix.dom
macros too). So you can do something like this:
(let [props {:on-click #(js/alert "clicked!")}]
($ MyComponent {:style {:color "red"} & props}))
This way, props are always written as a literal map and you can opt-in to dynamically setting props when needed. This also handles merging; props passed in via the &
key will be merged into the JS object generated by the literal props and override them.
So the way that you would handle your case would be like:
(defnc DraggableSelectableRow [{:keys [id index value cells-component]}]
(let [selected? (is-selected? id)]
($ Draggable {:draggableId id
:key id
:index index}
(fn [provided snapshot]
(d/tr {:ref (.-innerRef provided)
:class [(when selected? "table--selected")
(when (.-isDragging snapshot) "table-row-dragging")]
& (bean (.-draggableProps provided))}
($ table/SelectionCell {:id id :is-selected selected?})
($ cells-component {:value value})
(d/td {:class "table-row--draggable" & (bean (.-dragHandleProps provided))}
(d/span {:class "icon move"} (d/i)))))))
Some additional logic could be added to check if spread props are a map?
and if not, treat it like a JS object and merge it. That would remove the need for the bean
wrappers.
Good questions. I'll try and answer them all, but also include a little bit of background:
Components and elements
defnc
creates a standard functional React component that accepts a JS object and returns React Elements$
is a macro which creates a React element - you can give it any component type, e.g.($ "span" "hi")
. It will shallowly rewrite props maps to a JS obj, so you can use it with any React component (including external JS)helix.dom
helpers are simple factories built on top of$
. E.g.(d/div "hi")
is a macro for($ "div" ~@args)
In this way, there are no difference between an external JS component and a helix component, and no difference between a helix element and a React element created any other way (e.g. through hiccup parsing, or via JSX).
A thing that I think people will want to do (or at least I found myself doing) is creating factory macros for my components:
That way you can use it like so:
Re: compile-time checking of types passed into
$
, yes that's a good idea!Reader tags etc. are outside the scope of this lib, but you can easily use something like thump in place of the
$
macro; the only requirement is that components return React elements, how they are generated is up to you. Helix gives you$
andhelix.dom
to start but you can use thump, sablono, reagent, or any other solution for creating the elements.React interop is very similar to how
hx
is now, where you simply create the element the same as any other component. Render props work nicer when you're not using a dynamic hiccup parser. Here's an example:Hooks
The
use-effect
/use-memo
/use-callback
inference works by taking all the symbols inside the body passed to it and looking for any local bindings to those symbols. If it doesn't find a local binding for it, it won't include it. This in general is what you want, but sometimes you want to ignore certain values, in which case you can provide your own vector of deps. e.g.:(use-effect [foo] (+ foo bar))
To clarify,
^:callback
and^:memo
annotations are only for the expressions that are annotated; it wraps them inuse-callback
anduse-memo
hooks, and infers their dependency array. It does not have to do withReact.memo
(which memoizes a component).Example:
Pulling a key from a map is pretty contrived and doesn't need to be memoized, but it's a common thing that comes up when optimizing renders of child components and wanting to ensure that we keep referential identity when data hasn't changed.