Fork me on GitHub
#reagent
<
2020-04-26
>
juhoteperi09:04:31

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"

👍 16
aisamu12:04:59

> Shortcut to create functional component

😛 4
yenda12:04:51

does it mean that when using {:functional-components? true} you can't use ratoms anymore?

yenda13:04:02

I'd propose :fn for functional components, those who fancy fancy symbols can use ligatures

juhoteperi13:04:19

No, functional components work nearly the same as class components.

yenda13:04:28

by no you mean you can still use them? So what kind of breakings can one expect from enabling the option?

juhoteperi13:04:57

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.

yenda17:04:12

I meant in the first snippet of code

juhoteperi17:04:51

Ah missed that one

yenda17:04:05

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)

juhoteperi17:04:17

Did that work?

yenda17:04:20

seems like it worked very nicely I was able to remove my hacks for hooks

yenda17:04:28

and use hooks directly

yenda17:04:30

also context:

yenda17:04:40

(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)))])

juhoteperi17:04:50

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.

yenda17:04:15

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

yenda17:04:34

that was the only way I could find to have the props passed properly to the children

juhoteperi17:04:13

The last variant, with create-element doesn't convert properties

yenda17:04:27

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

yenda17:04:22

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)])))

yenda17:04:13

@U061V0GG2 correct me if I'm wrong but now props are passed without this issue right?

juhoteperi17:04:52

I don't think anything should affect how props are passed in this case.

juhoteperi17:04:54

Why do you need js->clj call?

yenda17:04:40

not needed I think

yenda17:04:56

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)}]))

juhoteperi17:04:50

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.

juhoteperi17:04:42

(per issue 494, I'll probably add another shortcut which would work like create-element)

yenda17:04:44

I didn't write this one so I'm not sure why it was using r/props instead of passing args

yenda17:04:50

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))

yenda17:04:35

very nice

yenda18:04:47

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

yenda18:04:42

mhm it still complains about hooks not being run in a functional component

juhoteperi18:04:02

In what case? If you use r/create-class that will obviously always create a class component

yenda18:04:11

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))))

yenda18:04:32

I think it was because view is a class from react-native

yenda18:04:23

yes view is (def view (r/adapt-react-class (.-View ^js rn))) so I need to call the hooks outside

yenda18:04:10

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

juhoteperi18:04:16

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.

yenda18:04:06

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>
  );
});

yenda18:04:18

that's how view is defined in react-native

yenda18:04:33

but looks like my hooks above are still not happy

yenda18:04:35

I'm wondering if that could be because (r/set-default-compiler! functional-compiler) is called too late

juhoteperi18:04:30

If you call it before reagent.dom/render call, should be fine

yenda19:04:39

but you don't use that in react-native

yenda19:04:58

you use (.registerComponent ^js react/app-registry "clash" #(r/reactify-component root))

yenda19:04:12

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)}))))

yenda19:04:35

I added set-default-compiler on top to test and it works with it

juhoteperi19:04:18

Well, before the reacitfy-component call in this case

yenda19:04:37

yeah but before is a complex notion here 😄

yenda19:04:34

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

yenda19:04:32

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

yenda19:04:06

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 ...

yenda19:04:35

then the compiler option doesn't apply unless you put it before that def

juhoteperi19:04:02

Instead of (only) calling set-default-compiler! you can also provide the compiler option to whatever is converting Hiccup-syntax to React elements.

juhoteperi19:04:17

Maybe as-element ... compiler inside create-class render

yenda19:04:45

(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)})))

yenda19:04:07

this is the macro, it works well now that I set compiler options in the cljs namespace of the macro

yenda19:04:51

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?

juhoteperi19:04:52

Ah well, in fact create-class takes compiler as optional second parameter.

yenda19:04:41

yes all works now

juhoteperi09:04:06

Aaand 1.0.0-alpha1 is out

🎉 40
yenda20:04:52

is there any perfomance impact (positive or negative) to usingthe new functional-components?compiiler options?

juhoteperi21:04:27

Not sure - I did some very simple benchmarks but didn't find much impact.

juhoteperi21:04:27

Not sure - I did some very simple benchmarks but didn't find much impact.