I've finally merged the functional component support to Reagent master: https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#features-and-changes alpha release will follow "soon"
I've written some docs and examples about the new features: https://github.com/reagent-project/reagent/blob/master/doc/ReagentCompiler.md https://github.com/reagent-project/reagent/blob/master/doc/ReactFeatures.md#function-components https://github.com/reagent-project/reagent/blob/master/examples/functional-components-and-hooks/src/example/core.cljs And I still have one idea about making it easy to use functional components even with default options: https://github.com/reagent-project/reagent/issues/494
Aaand 1.0.0-alpha1 is out
> Shortcut to create functional component
:λ
does it mean that when using {:functional-components? true}
you can't use ratoms anymore?
I'd propose :fn
for functional components, those who fancy fancy symbols can use ligatures
No, functional components work nearly the same as class components.
by no you mean you can still use them? So what kind of breakings can one expect from enabling the option?
Ratoms work. Hard to say, r/current-component
returns a mocked object as there is no real Component instance, so if you use that to access component this
and do something with that, that will probably break.
r/render doesn't exist here https://github.com/reagent-project/reagent/blob/master/doc/ReagentCompiler.md#reagent-compiler ?
Fixed
I meant in the first snippet of code
Ah missed that one
for react-native I didn't touch the current code (r/reactify-component root)
and used
(def functional-compiler (r/create-compiler {:functional-components? true}))
(r/set-default-compiler! functional-compiler)
Did that work?
seems like it worked very nicely I was able to remove my hacks for hooks
and use hooks directly
also context:
(defn top-safe-area-view
[props & children]
[:> SafeAreaConsumer {}
(fn [insets]
(r/as-element
(into [view (assoc-in (js->clj props) [:style :padding-top] (.-top insets))]
children)))])
(defn bottom-safe-area-view
[props & children]
[:> SafeAreaConsumer {}
(fn [insets]
(r/as-element
(into [view (assoc-in (js->clj props) [:style :padding-bottom] (.-bottom insets))]
children)))])
(defn keyboard-avoiding-view
[props & children]
[:> Header-height-context-consumer {}
(fn [header-height]
(r/as-element
(into [keyboard-avoiding-view-class
(cond-> (js->clj props)
platform/ios? (assoc :behavior :padding
:keyboardVerticalOffset header-height))]
children)))])
Phew nice. I've quickly tested this with some work projects and I've seen some warnings at some point, but didn't test the latest version yet.
previously I had to use a hack with a ratom where I rendered the hook in a dummy function and reset! the atom in it to pass to the view
that was the only way I could find to have the props passed properly to the children
Did you see this context example I added a month ago: https://github.com/reagent-project/reagent/blob/master/examples/react-context/src/example/core.cljs
The last variant, with create-element
doesn't convert properties
yes but previously when using as-element the props where not passed down correctly the namespaced keywords lose their namespaces and the sets become vectors
this was my workaround to preserve args 😄
(defn top-safe-area-view
[props & children]
(let [top-inset (r/atom nil)]
(fn [props & children]
[:<>
[:> SafeAreaConsumer {}
(fn [insets]
(reset! top-inset (.-top insets))
nil)]
(into [view (assoc-in (js->clj props) [:style :padding-top] @top-inset)]
children)])))
@juhoteperi correct me if I'm wrong but now props are passed without this issue right?
I don't think anything should affect how props are passed in this case.
Why do you need js->clj
call?
not needed I think
this is the code that caused an issue:
(defn- keyboard-avoiding-view-element [args]
(let [height (useHeaderHeight)
{:keys [props children]} (js->clj args :keywordize-keys true)]
(r/as-element
(into [keyboard-avoiding-view* (cond-> (js->clj props)
platform/ios? (assoc :behavior :padding
:keyboardVerticalOffset height))]
children))))
(defn keyboard-avoiding-view
"custom component, to be used before vanilla RN KeyboardAvoidingView is not fixed on ios devices with a notch"
[]
(let [this (r/current-component)]
[:> keyboard-avoiding-view-element {:props (r/props this)
:children (r/children this)}]))
If use use :>
to call keyboard-avoiding-view-element
props are converted to JS objects, which is inconvenient here as it is Cljs function. You could use create-element
directly to preserve Cljs values, then the other components would probably be simpler.
(per issue 494, I'll probably add another shortcut which would work like create-element
)
I didn't write this one so I'm not sure why it was using r/props instead of passing args
but now I can just do this with reagent 1
(defn top-safe-area-view
[props & children]
(into [view (assoc-in props
[:style :padding-top]
(.-top (useSafeArea)))]
children))
(defn bottom-safe-area-view
[props & children]
(into [view (assoc-in props
[:style :padding-bottom]
(.-bottom (useSafeArea)))]
children))
(defn keyboard-avoiding-view
[props & children]
(into [keyboard-avoiding-view-class
(cond-> props
platform/ios? (assoc :behavior :padding
:keyboardVerticalOffset (useHeaderHeight)))]
children))
very nice
If use use :> to call keyboard-avoiding-view-element props are converted to JS objects, which is inconvenient
yes I think that was the issue, as the conversion is lossy for namespace keywords and sets, and as a result any view that was using this component ended up with subtle bugs because of that
mhm it still complains about hooks not being run in a functional component
In what case? If you use r/create-class
that will obviously always create a class component
but the following works
(defn top-safe-area-view
[props & children]
(let [^js insets (useSafeArea)]
(fn [props & children]
(into [view (assoc-in props
[:style :padding-top]
(.-top insets))]
children))))
(defn bottom-safe-area-view
[props & children]
(let [^js insets (useSafeArea)]
(fn [props & children]
(into [view (assoc-in props
[:style :padding-bottom]
(.-bottom insets))]
children))))
(defn keyboard-avoiding-view
[props & children]
(let [header-height (useHeaderHeight)]
(fn [props & children]
(into [keyboard-avoiding-view-class
(cond-> props
platform/ios? (assoc :behavior :padding
:keyboardVerticalOffset header-height))]
children))))
I think it was because view is a class from react-native
yes view is (def view (r/adapt-react-class (.-View ^js rn)))
so I need to call the hooks outside
thank you so much for this work being able to use hooks so easily with the current context of the js ecosystem using hooks everywhere now is a game changer
adapt-react-class
name can be a bit confusing, the React component could be a function and the call won't change it to a class. But not sure if View is class or Fn here.
const View: React.AbstractComponent<
ViewProps,
React.ElementRef<typeof ViewNativeComponent>,
> = React.forwardRef((props: ViewProps, forwardedRef) => {
return (
<TextAncestor.Provider value={false}>
<ViewNativeComponent {...props} ref={forwardedRef} />
</TextAncestor.Provider>
);
});
that's how view is defined in react-native
but looks like my hooks above are still not happy
I'm wondering if that could be because (r/set-default-compiler! functional-compiler) is called too late
If you call it before reagent.dom/render
call, should be fine
but you don't use that in react-native
you use (.registerComponent ^js react/app-registry "clash" #(r/reactify-component root))
yep seems like it's the issue, I have a macro to def screens like this
([name component]
`(def ~name
(do (reagent.core/set-default-compiler! (reagent.core/create-compiler {:functional-components? true}))
(reagent.core/create-class
{:reagent-render
(fn []
@app.navigation/cnt
;;NOTE this can be useful to debug re-rendering
~(when log-react-lifecycles
`(taoensso.timbre/debug ~(str "rerender screen: " *ns* "/" name)))
~component)}))))
I added set-default-compiler on top to test and it works with it
Well, before the reacitfy-component
call in this case
yeah but before is a complex notion here 😄
I think that because my defscreen macro is doing a def create-class the create-class is exectuted before the reactify-component, and before set-default-compiler is ran because those are in the core namespace, and it is evaluated last
as for why I'm doing that, the reason is that react-navigation is dynamically setting up the routes, and I just wanted to define the screens only once, so that when remounting a route there is no need to recreate the whole screen class again
I suppose it might happen whenever you have a lib that takes a component as argument, and you decide to do a def (reagent/create-class ...
then the compiler option doesn't apply unless you put it before that def
Instead of (only) calling set-default-compiler!
you can also provide the compiler option to whatever is converting Hiccup-syntax to React elements.
Maybe as-element ... compiler
inside create-class render
(defmacro defscreen
[name component]
`(def ~name
(reagent.core/create-class
{:reagent-render
(fn []
@app.navigation/cnt
;;NOTE this can be useful to debug re-rendering
~(when log-react-lifecycles
`(taoensso.timbre/debug ~(str "rerender screen: " *ns* "/" name)))
~component)})))
this is the macro, it works well now that I set compiler options in the cljs namespace of the macro
not sure where i'd put as-element here, would it make sense to have the option to set it in create-class as well?
Ah well, in fact create-class takes compiler as optional second parameter.
awesome
yes all works now
is there any perfomance impact (positive or negative) to usingthe new functional-components?
compiiler options?