lilactown / helix Goto Github PK
View Code? Open in Web Editor NEWA simple, easy to use library for React development in ClojureScript.
License: Eclipse Public License 2.0
A simple, easy to use library for React development in ClojureScript.
License: Eclipse Public License 2.0
Provide a helix.core/clone-element
macro which uses the same props conversion API as $
.
I'm getting the following compile warnings when using Figwheel - these hook macros are defined outside a reader conditional, but depend on things only defined under :clj
:
deps-macro-body
is only defined for :clj
:
use-effect Use of undeclared Var helix.hooks/deps-macro-body at line 156, column 4 in file helix\hooks.cljc
use-layout-effect Use of undeclared Var helix.hooks/deps-macro-body at line 176, column 4 in file helix\hooks.cljc
use-memo Use of undeclared Var helix.hooks/deps-macro-body at line 196, column 4 in file helix\hooks.cljc
use-callback Use of undeclared Var helix.hooks/deps-macro-body at line 224, column 4 in file helix\hooks.cljc
use-imperative-handle Use of undeclared Var helix.hooks/deps-macro-body at line 243, column 4 in file helix\hooks.cljc
hana
is only imported under :clj
:
use-memo No such namespace: hana, could not locate hana.cljs, hana.cljc, or JavaScript source providing "hana" helix\hooks.cljc line:203 column:16
Use of undeclared Var hana/inferred-type helix\hooks.cljc line:203 column:16
Use of undeclared Var hana/inferred-type helix\hooks.cljc line:209 column:16
I'm using fix-figwheel :sha "43ed17b2828ae7f5b406f99d5dbb753321bf73d6"
.
Stub generation fails in Intellij Cursive with the following error:
Error generating stubs for module demo-app: clojure.lang.ExceptionInfo: Library name must be specified as a symbol in :require / :require-macros; offending spec: ["react" :as react] at line 1 jar:file:/Users/emmanuel.john/.m2/repository/lilactown/helix/0.0.13/helix-0.0.13.jar!/helix/core.cljs {:file #object[java.net.URL 0x790ed2d9 "jar:file:/Users/emmanuel.john/.m2/repository/lilactown/helix/0.0.13/helix-0.0.13.jar!/helix/core.cljs"], :line 1, :column 1, :tag :cljs/analysis-error}
at cljs.analyzer$error.invokeStatic(analyzer.cljc:645)
at cljs.analyzer$error.invoke(analyzer.cljc:641)
at cljs.analyzer$error.invokeStatic(analyzer.cljc:643)
at cljs.analyzer$error.invoke(analyzer.cljc:641)
at cljs.analyzer$basic_validate_ns_spec.invokeStatic(analyzer.cljc:2008)
at cljs.analyzer$basic_validate_ns_spec.invoke(analyzer.cljc:1999)
at cljs.analyzer$parse_require_spec.invokeStatic(analyzer.cljc:2107)
at cljs.analyzer$parse_require_spec.invoke(analyzer.cljc:2103)
at clojure.lang.AFn.applyToHelper(AFn.java:171)
at clojure.lang.AFn.apply... (show balloon)
I'm using the following deps:
org.clojure/clojurescript {:mvn/version "1.10.773"}
thheller/shadow-cljs {:mvn/version "2.11.6"}
lilactown/helix {:mvn/version "0.0.13"}
Cursive apparently do not like this. Is it possible to change the 'require' to use symbols only where possible?
I propose that $
support the following: ($ "div.foo.bar#unique#id" {} β¦)
where {:class ["foo" "bar"] :id "unique id"}
would be set as a result of this.
Many hiccup-inspired tools utilize this kind of syntax, for example Reagent. The shorthand makes it a lot easier to sell a team on Helix that is currently using something with "less typing".
We've been using a $-wrapping macro at work since using Helix (~6 months at least) and have not found any downsides yet. Our code is definitely shorter! :)
I just looked at advanced mode output and $
macros tend to emit a lot of calls to helix.core/create-element
.
Unfortunately currently this does not inline well in advanced mode. Dynamic arity dispatch code is generated everywhere.
I started some investigation, but didn't find proper solution to refer to react/createElement directly from a macro (might be shadow-cljs issue):
binaryage/cljs-react-three-fiber@1d23a50
I couldn't get either sablono or hicada to work with React Native elements, and I always had good success with hx
, so I've been using this defnc macro:
(defmacro defnc [type params & body]
(let [default-opts {:helix/features {:fast-refresh true}}
[opts body] (if (map? (first body))
[(first body) (rest body)]
[{} body])]
`(helix.core/defnc ~type ~params
~(merge default-opts opts)
(hx.react/f (do ~@body)))))
Just wanted a sanity check that hx.react/f
is the right way to wrap hiccup bodies.
I could also create a PR adding a note to docs/react-native that this seems to be the best way to get hiccup working for helix + react-native.
This breaks in the latest clojurescript version.
Helix should expose helper fns that make dealing with children more idiomatic.
See https://reactjs.org/docs/react-api.html#reactchildren
helix.children.map
helix.children.doseq
helix.children.count
helix.children.only
helix.children.vec
I think this should be trivial to fix after #86 is merged. Right now, hooks can't be found when they hide inside pesky #js
tagged literals like so:
(when true
#js [(use-state 10)])
Both the find-hooks
and invalid-hooks-usage
functions in the analyser need updating to know how to descend into a JSValue.
I cannot get a working fast-refresh example, unless I misunderstand what it should give me.
#38 is my attempt.
Hello,
I found a bug in the $
with native components.
Reproduce example:
($ "div" {:style #js {:flex 1}})
This gets an error of Error: [object Object] is not ISeqable
I did a little dig, the problem seems to be in this line here: https://github.com/Lokeh/helix/blob/37aca5e65704da96915a4435e6825da4e4de3af7/src/helix/impl/props.cljc#L57
At my example, this ends up calling seq
with a JS object, which triggers the error.
Sometimes you get a JS object and want to forward it as props to a component.
What we'd like to do:
(defnc my-component []
;; `some-lib/Foo` is an external lib that uses a common pattern:
;; pass it a function-as-children which it then passes props to as a JS object
($ some-lib/Foo
(fn [foo-props]
(d/div {& foo-props} "I'm so foo-y!"))))
Currently, this requires converting or wrapping foo-props
so that it is a CLJS map-alike, so that spread props works.
(defnc my-component []
($ some-lib/Foo
(fn [foo-props]
;; wrap it in a bean, so that it can then be converted into a JS obj again... :sad:
(d/div {& (cljs-bean.core/bean foo-props)} "I'm so foo-y!"))))
To support writing spread props like in the first snippet, spread props should check to see if it's a map-alike and if not, fall back to merging it in by Object.keys
.
When testing with web, transitive namespaces do not trigger a refresh. Example:
(ns app.a
(:require
[app.b]
[helix.core :refer [defnc]])
(defnc app
[]
(app.b/greet "Refresh"))
(ns app.b
(:require
[app.c]))
(defn greet
[name]
(str app.c/greeting ", " name "!"))
(ns app.c)
(def greeting "Bonjour")
Editing app.c/greeting
to "Hello"
will not trigger app.a/app
to refresh.
I'm not sure if this behavior is different in React Native; it is dependent on whether app.a gets reloaded when transitive namespaces are edited. shadow-cljs on the web currently only reloads namespaces that directly depend on the edited namespace.
Thheller:
the issue is that in development react "validates" the elements it gets immediately, walking the entire structure and thus forcing lazy seqs
in production that may be delayed since it doesn't validate
so the "work" potentially happens at different times. it is known to cause problems if you use any kind of bindings
Doesn't work: (my-component {:key 10})
. Does work ($ (helix/type my-component) {:key 10})
.
example usage:
(defnc App []
($ rn/View
{:style {:background "blue"}}
($ rn/Text "Hello World!")))
i'm receiving the following error:
Failed prop type: Invalid props.style key `meta` supplied to `View`.
Bad object: {
"meta": null,
"cnt": 1,
"shift": 5,
"root": {
"edit": null,
"arr": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
]
},
"tail": [
{
"meta": null,
"cnt": 1,
"arr": [
{
"ns": null,
"name": "flex",
"fqn": "flex",
"_hash": -1425124628,
"cljs$lang$protocol_mask$partition0$": 2153775105,
"cljs$lang$protocol_mask$partition1$": 4096
},
1
],
"__hash": null,
"cljs$lang$protocol_mask$partition0$": 16647951,
"cljs$lang$protocol_mask$partition1$": 139268
}
],
"__hash": null,
"cljs$lang$protocol_mask$partition0$": 167666463,
"cljs$lang$protocol_mask$partition1$": 139268
}
however if i print output of component it looks fine (see below) so not sure why inline styles aren't working in react native if they work in regular dom (they both accept object styles, though react native also accepts an array of styles)
#js {"$$typeof" "Symbol(react.element)",
:type #object[View],
:key nil,
:ref nil,
:props #js {:style {:background "blue"},
:children #js { ... }
I'm trying to get started with helix and react-native, and am especially interested in getting react fast refresh working.
I couldn't get fast-refresh to work with helix 0.0.8. Perhaps someone could point me in the right direction toward getting fast refresh to work?
I tried to boil things down to a minimal example repo here: https://github.com/aiba/helix-react-native
uix library also does props translation and uses cache[1] - prop names translation might be a hot code path.
I think helix should explore this possibility as well. I don't think it is that important for helix because 99% of time props get translated statically and is not worth optimizing of the rest of dynamic cases.
But in the other direction bean is dynamic so caching there would make more sense. So we could keep only cache from js names back to cljs keywords.
A prerequisity for this should be to have some reliable benchmarks to decide if this would be worth it.
Currently, using a JS tagged literal i.e. #js []
inside of a hook will bust the react-refresh cache every time.
This is due to the fact that #js []
emits a value created via deftype
, and then helix coverts this at compile time to a string to use as the cache key. The stringified object contains a memory reference, which is different every time.
Copying @SevereOverfl0w 's slack investigation here for posterity:
dominicm 1:09 PM
Ah, string/join runs in clojure-lang. Derp! Right, I bet it's getting something that looks very different then!
dominicm 1:10 PM
"hooks-key" "(hooks/use-state (do #object[cljs.tagged_literals.JSValue 0x73ca9fc4 \"cljs.tagged_literals.JSValue@73ca9fc4\"] \"Lisa\"))"
That's why! It's printing out the object using the JSValue, which is an object so has a memory reference associated @alex J Henderson @lilactown here's the bug π
dominicm 1:21 PM
(string/join (map cljs.compiler/emit-constant hooks))
does the trick. But it's pretty verbose. Based on a cursory look around the source, you might only need to solve for regex/JSValue (based on the fact those are both things that broke Cljs' own caching in the past, and are still the only 2 custom ones: https://github.com/clojure/clojurescript/blob/9d3dfc369a01b31244eb925fef4c9fafa3824e24/src/main/clojure/cljs/analyzer.cljc#L94-L103).
As far as I can tell, JSValue is the only deftype in a cljc file in ClojureScript, so special casing JSValue probably makes sense.
(string/join
(clojure.walk/postwalk
(fn [x]
(if (instance? JSValue x)
(str "#js" (.val x) "")
x))
hooks))
Also works if you just special case JSValue types. (edited)
This feature was broken in this commit 0ac5b18
One problem with the use-memo / use-effect / et al. macros is that their static analysis does not extend to custom hooks.
For instance, you cannot define a custom hook that uses the :auto-deps
functionality without creating a macro and using some internal functions of helix.
The proposal here is to introduce a new macro (or two) with semantics that would allow users to define custom hooks that benefit from helix's static analysis.
;; using a built in hook
(use hooks/effect
:once
(foo))
;; using a custom hook
(use my-custom-hook
:auto-deps
#(foo))
Custom hooks could be defined using a defhook
macro that could annotate the symbol with info about whether it takes deps or not.
(defhook my-custom-hook
[^:deps deps f]
(use hooks/effect
deps
(f)))
We discussed on slack a little while ago. This would be useful for something we're working on right now (dynamic dispatch of components based on server-side information) so I thought I'd create an issue to track it.
We're able to work without the anonymous stuff for now, so no urgency from my side.
Create a macro that will define new hooks, applying lint rules for rules of hooks as well as adding the ability to annotate certain behavior with metadata.
(defhook use-thing
[foo bar]
,,,)
This can be a building block towards:
Since this would be used only in dev mode. This should be done on client side for better flexibility.
Something along these lines:
darwin@e43f4b7
binaryage/cljs-react-three-fiber@eb5b81d#diff-2f8098cbd9d667f556f10988d437ff8f
trying to use helix with shadow-cljs and getting following error
--- helix/impl/class.js:1
Namespace imports (goog:some.Namespace) cannot use import * as. Did you mean to import React from 'goog:shadow.js.shim.module$react';?
It appears that using a value of nil
in the spread operator / dynamic props results in a runtime error as an attempt is made to index into the nil map.
A minimal reproduction is:
(let [x nil]
($ :div {& x}))
Seemingly, calling ($ :div {& nil})
doesn't trigger the issue.
The exception is thrown on this line:
The error appears as so:
It would be nice to gracefully handle this case as though an empty map were passed, in line with other behaviour throughout Clojure. For now this can be mitigated by constructing the component using the form:
($ :div {& (or x {})})
defnc
should detect if dependencies are exhaustively filled in, and if not signal a compiler warning. It could be silenced by filling in the dependencies or annotating with metadata.
;; Warning! Non-exhaustive dependencies
(use-memo
[foo]
(+ foo bar))
;; Ignore specific dependency in the body
(use-memo
^{:ignore-deps [bar]} [foo]
(+ foo bar))
;; Don't warn ever
(use-memo
^:ignore-deps []
(+ foo bar))
Lines 131 to 137 in 51e758e
If I define a factory component, that also uses a HOC which provides props (in this case withTooltip https://airbnb.io/visx/docs/tooltip which the docs say are deprecated but is still used in the examples) then the extra props provided by the HOC aren't included.
Maybe the props need to be bean'd as well?
(merge (bean/bean o) (assoc-some props :children (gobj/get o "children")))
I have a component that accepts string shorthands in style vector, e.g. {:style ["flex-1"]}
, but errors out with Helix since its wrapping all vector items in primitive-obj
.
clojure.spec would be very useful as a way to document props and assert at runtime that they are correct.
There are two fundamental issues that I would want to address by providing direct support for clojure.spec:
A rough sketches of a potential API:
(ns app.feature
(:require
[clojure.spec.alpha :as s]
[helix.core :refer (defnc $)]
[helix.dom :as d]
[helix.spec :as hs]))
;; can use regular specs
(s/def ::name string?)
;; `hs/def` is just like `s/def` but will be removed if goog/DEBUG is false
(hs/def ::on-name-change function?)
;; like `s/fdef` but for components
(hs/cdef greeting
:props (s/keys :req-un [::name ::on-name-change]))
;; specs are separate from components, can be added on later or removed w/o
;; any change to component.
;; Later maybe add specific syntax for speccing inline?
(defnc greeting
[{:keys [name on-name-change]}]
(d/div
(d/div βHello, β name β!β)
(d/input {:value name :on-change on-name-change})))
I find this library great enough already to use in an app I'm building. It would be really helpful to have a changelog file between releases. Would the maintainer be keep a log of notable changes?
(defn use-custom [deps f]
(use-effect
deps
(f)))
At the moment, deps
will be silently ignored and this use-effect
macro will emit nothing.
Should fall back to runtime detection of :once
/ :always
/ a vector.
&
is a special symbol acting as a spread operator in $
macro expansion.
($ :div {:prop "val" & rest-props} ...)
This causes minor issue in Cursive which sees a normal map and complains about unresolved symbol &
. I think adding support for :&
to be optionally used in place of &
should not hurt anynone.
($ :div {:prop "val" :& rest-props} ...)
See darwin@be5c3dc
We could remove a lot of the work involved with using different renderers (react-three-fiber, react-native, etc.) and make interop slightly easier if we defaulted to always converting kebab-case to camelCase when using $
/ $$
, and always transforming camelCase to kebab-case in defnc
body via a bean with custom key->prop
and prop->key
as suggested by @darwin.
There might still need to be some determination of whether an element is "native" in order to decide whether to recursively convert the :style
key, but this could be handled by helix.dom
and / or other wrapper macros on a per-case basis.
After this commit, when the :value
prop is nil
, helix would replace it with undefined
. But in react components like input
etc. with :value nil
is treated as a controlled component, while :value undefined
is treated as uncontrolled.
This is a problem e.g. when using the Autocomplete
component of material-ui, the :value
prop must be either nil
or one of the values in the options list. However when I try to give it a nil
, helix changes it to undefined
, which effectively make this component a uncontrolled one. Later when the user chooses a value from the auto complete list, the component would become a controlled element and cause errors.
I am installing helix
into an app that is already using hx
I have included the library as so:
lilactown/helix {:mvn/version "0.0.6"}
Requiring [helix.core :refer [defnc]]
throws the error bellow:
[Figwheel:WARNING] Could not Analyze: /_SLASH_impl_SLASH_class/js.cljs is not a relative path resources/public/js/helix/core.cljs [Figwheel:SEVERE] java.lang.IllegalArgumentException: /_SLASH_impl_SLASH_class/js.cljs is not a relative path
This basically applies to requiring anything.
Enable https://github.com/Lokeh/helix/blob/master/docs/experiments.md#detect-invalid-hooks-usage by default in defnc
components and in defhook
.
Requires testing and filling in any remaining gaps.
Deprecate $$
and replace it with a CLJS function $
when it needs to be taken as a value.
I see that helix.dom/marker
is undeclared. (Ref: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker)
Was this simply missed in the list of tags in helix/dom.cljc
, or is there a deeper reason for the exclusion?
Currently, we maintain a separate branch for using helix with vanilla CLJS (e.g. figwheel, :bundle
, etc.): fix-figwheel
This is due to src/helix/impl/class.js
originally being written using ES Module syntax (as that worked well with shadow-cljs), and when I tried to rewrite it using Google Closure syntax it didn't immediately work with shadow-cljs.
Ideally we would align the two branches so that it's less confusing for users.
Hi @Lokeh! Thank you for Helix!
I'm struggling to use fast-refresh with figwheel-main. I managed to add a ns to :preloads
which injects a hook and set up a callback when code changes. Basically something like this:
(ns ^:figwheel-hooks fast-refresh
(:require [helix.experimental.refresh :as r]))
(r/inject-hook!)
(defn ^:after-load refresh
[]
(r/refresh!))
I know the refresh
callback is invoked as it shows the component in the "updated" list but nothing changes on screen. The code is not re-rendered.
I used react 16.13.1, helix 0.0.11, and react-refresh 0.8.2.
Is there anything else needed? Am I missing something?
And of course I will drop a PR with documentation changes when I make it work :) π
Or at least document that helix doesn't support IE.
We should be able to signal that an expression should be wrapped in a use-memo
or use-callback
by annotating it with metadata in the body of a defnc
or a defhook
.
(defnc my-component
[{:keys [value]}]
^:memo (d/div value)) ;; returns the same react element for the save `value`
^:memo
and ^:callback
metadata should tell defnc
and defhook
to emit a use-memo
or use-callback
hook that will automatically fill dependencies using the same algorithm as :auto-deps
.
A dependency seq can also be provided to override this behavior:
(defnc my-component
[{:keys [foo bar baz]}]
^{:memo [foo]} (d/div value)) ;; ignores changes to `bar` and `baz`
^:memo
and ^:callback
metadata should be usable in let
bindings:
(defnc my-component
[{:keys [value]}]
(let [calculated ^:memo (+ value 10)
on-click ^:callback #(js/alert calculated)]
^:memo (d/button {:on-click on-click} "Click me!")))
^:memo
and ^:callback
should allow enforcement of the Rules of Hooks checks that are currently behind an experimental feature flag.
When completing you get both -render-type and -item-render-type for the component. I think these should be private instead, thoughts?
Since we create the inner fn of the component with the same name as var we are def
ing, if you wrap the component in an HOC and call it recursively you end up getting the unwrapped component π
example psuedo-code of the bug:
(def node-view (-> (fn node-view [props]
;; this refers to the inner fn, not the top-level def node-view!!
($ node-view ,,,))
(helix.core/memo)))
Hello,
I've just noticed that if I define a component like
(helix.core/defnc Foo [{:keys [bar baz]}]
{:helix/features {:define-factory true}}
($ "p" bar baz))
then I cannot pass through props like
(let [props {:baz "test"}]
(Foo {:bar "yo" :& props}))
instead I must
(let [props {:baz "test"}]
(Foo (merge {:bar "yo"} props)))
Is this a limitation of factory functions or just a bug? I can live without :&
on factory functions if they are not possible, but maybe this should be documented at docs/creating-elements.md#factory-functions
?
Thank you! π
What is the difference between hx and helix?
I think the community could get confused, maybe could add this difference in the Readme
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.