reagent

A minimalistic ClojureScript interface to React.js http://reagent-project.github.io/
juhoteperi 2020-04-26T09:03:31.257700Z

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"

4👍
juhoteperi 2020-04-26T09:36:06.259200Z

Aaand 1.0.0-alpha1 is out

10🎉
aisamu 2020-04-26T12:31:59.259800Z

> Shortcut to create functional component

1😛
yenda 2020-04-26T12:56:51.260100Z

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

yenda 2020-04-26T13:00:02.260300Z

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

juhoteperi 2020-04-26T13:03:19.260500Z

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

yenda 2020-04-26T13:04:28.260700Z

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

juhoteperi 2020-04-26T13:07:57.260900Z

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.

yenda 2020-04-26T13:31:25.261800Z

r/render doesn't exist here https://github.com/reagent-project/reagent/blob/master/doc/ReagentCompiler.md#reagent-compiler ?

juhoteperi 2020-04-26T17:29:18.262500Z

Fixed

yenda 2020-04-26T17:43:12.262700Z

I meant in the first snippet of code

juhoteperi 2020-04-26T17:43:51.262900Z

Ah missed that one

yenda 2020-04-26T17:44:05.263100Z

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)

juhoteperi 2020-04-26T17:44:17.263300Z

Did that work?

yenda 2020-04-26T17:44:20.263500Z

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

yenda 2020-04-26T17:44:28.263700Z

and use hooks directly

yenda 2020-04-26T17:44:30.263900Z

also context:

yenda 2020-04-26T17:44:40.264100Z

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

juhoteperi 2020-04-26T17:44:50.264300Z

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.

yenda 2020-04-26T17:45:15.264500Z

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

yenda 2020-04-26T17:45:34.264700Z

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

juhoteperi 2020-04-26T17:46:40.264900Z

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

juhoteperi 2020-04-26T17:47:13.265200Z

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

yenda 2020-04-26T17:47:27.265400Z

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

yenda 2020-04-26T17:48:22.265600Z

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

yenda 2020-04-26T17:49:13.265800Z

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

juhoteperi 2020-04-26T17:49:52.266Z

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

juhoteperi 2020-04-26T17:50:54.266200Z

Why do you need js->clj call?

yenda 2020-04-26T17:51:40.266400Z

not needed I think

yenda 2020-04-26T17:51:56.266600Z

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

juhoteperi 2020-04-26T17:56:50.266800Z

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.

juhoteperi 2020-04-26T17:57:42.267Z

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

yenda 2020-04-26T17:57:44.267200Z

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

yenda 2020-04-26T17:58:50.267400Z

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

yenda 2020-04-26T17:59:35.267700Z

very nice

yenda 2020-04-26T18:08:47.268Z

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

yenda 2020-04-26T18:20:42.268200Z

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

juhoteperi 2020-04-26T18:23:02.268400Z

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

yenda 2020-04-26T18:23:11.268600Z

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

yenda 2020-04-26T18:23:32.268800Z

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

yenda 2020-04-26T18:24:23.269Z

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

yenda 2020-04-26T18:26:10.269200Z

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

juhoteperi 2020-04-26T18:27:16.269400Z

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.

yenda 2020-04-26T18:30:06.269600Z

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

yenda 2020-04-26T18:30:18.269800Z

that's how view is defined in react-native

yenda 2020-04-26T18:30:33.270Z

but looks like my hooks above are still not happy

yenda 2020-04-26T18:57:35.270300Z

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

juhoteperi 2020-04-26T18:59:30.270500Z

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

yenda 2020-04-26T19:08:39.270700Z

but you don't use that in react-native

yenda 2020-04-26T19:08:58.270900Z

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

yenda 2020-04-26T19:11:12.271100Z

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

yenda 2020-04-26T19:11:35.271300Z

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

juhoteperi 2020-04-26T19:25:18.271500Z

Well, before the reacitfy-component call in this case

yenda 2020-04-26T19:27:37.271700Z

yeah but before is a complex notion here 😄

yenda 2020-04-26T19:29:34.271900Z

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

yenda 2020-04-26T19:33:32.272100Z

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

yenda 2020-04-26T19:37:06.272300Z

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

yenda 2020-04-26T19:37:35.272500Z

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

juhoteperi 2020-04-26T19:38:02.272700Z

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

juhoteperi 2020-04-26T19:38:17.272900Z

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

yenda 2020-04-26T19:38:45.273100Z

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

yenda 2020-04-26T19:39:07.273300Z

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

yenda 2020-04-26T19:39:51.273500Z

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?

juhoteperi 2020-04-26T19:42:52.273700Z

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

yenda 2020-04-26T19:43:07.273900Z

awesome

yenda 2020-04-26T19:44:41.274100Z

yes all works now

yenda 2020-04-26T20:53:52.274800Z

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