nosco / hx

A simple, easy to use library for React development in ClojureScript.
MIT License
246 stars 16 forks source link

Experience Report #50

Open tomconnors opened 5 years ago

tomconnors commented 5 years ago

Hi, I spent the past couple days integrating hx with an existing Reagent application and thought I'd share an experience report in case it's useful to other users or to the library developers.

First, the reason I did this at all is that Reagent's development has slowed while React continues to introduce new features and idioms, and I would like to be able to use javascript libraries that use new React features like hooks without difficulty. The application currently uses re-frame and reagent.

I tried to replace a couple UIs with hx, and it ended up not being very difficult at all.

Because I have some library components for form inputs and things like that, I need to manage reagent components containing hx components and vice versa. This is pretty easy in both directions. Reagent components can return react elements, so instead of using the typical [my-reagent-component and its args] that you'll see a lot in reagent applications, you do (hx.react/$ my-hx-component args-as-a-map). The other direction requires you to use reagent.core/as-element. Instead of referring to the subcomponent in the usual hx way, [my-hx-component props-map], you use (reagent/as-element [my-reagent-component ...]).

I ended up adding some wrapper components to deal with the fact that reagent components can take any number of args but hx components just take a single props map.

(defn reagent-wrapper-of-hx-component [whatever arguments etc]
  (hx/$ whatever {:whatever whatever :arguments arguments :etc etc}))

I have been using https://github.com/Lokeh/reagent-context in this project for react context + reagent integration. That lib doesn't directly expose the react Context object when you create a context, so in order to use the useContext hook, I had to dig into the implementation details a bit:

(def ctx (reagent-context/create))
;; in a let binding in an hx component:
(react/useContext (.-instance ctx))

This is a re-frame project, so I've used subscribe and dispatch extensively. Nothing needed to change in calls to dispatch but subscribe calls were replaced with calls to use-sub, as defined here (note it's defined as <-sub in that repo).

(let [thing @(rf/subscribe [:thing])])
;; becomes:
(let [thing (use-sub [:thing])])

Also, don't forget to change reagent components using the hiccup class+id shorthand to define those things in props. I don't like the #id.class syntax in hiccup, but I haven't totally gotten rid of it from my project yet.

[:div.foo]
;; becomes
[:div {:class "foo"}]

I've ended up needing to work w/ js objects and idioms more than I would have hoped, plus I worry that the cljs<->js translation that happens w/ defnc components will eventually cause perf problems. It makes me wonder how practical/achievable it would be to modify react to allow things other than plain js objects for props. It's also possible I overused plain js objects because I wasn't always sure where clojurescript objects were acceptable/would perform well, like in the useState hook. IDK if that is something that hx should try to document.

I also integrated react-dnd with hx, which was easier than when I tried to use it with Reagent before. I know others have had success with a reagent and react-dnd integration via a wrapper lib, but I was able to use react-dnd without any wrapper lib at all, which is nice.

I'm pleased so far with hx, and will do future js library integrations via hx instead of reagent, and perhaps eventually replace all my reagent code with hx.

Only complaint: some of the names in hx's API are unclear. hx.react/f and hx.react/$ are the main offenders. I'd prefer longer, descriptive names. Especially given how infrequently those functions are used in practice.

DjebbZ commented 5 years ago

Thanks for the report. Very happy to hear a successful story.

lilactown commented 5 years ago

I appreciate the time you put into writing this up! I'm glad you're feeling productive in it and that it's solving a pain point for you with other libraries.

A couple things that might help:

  1. Regarding Context: If you're using reagent-context, it might be easier to construct the Context value using react/createContext (or hx.react/createContext which is just a proxy for it) and then create a reagent-context type using reagent-context.core/->Context. Example:
(def my-app-context (react/createContext))

(def ctx (reagent-context.core/->Context my-app-context))

(hx.react/defnc [props]
  (let [ctx-value (react/useContext my-app-context)]
     [:div (prn ctx-value)]))

(reagent-context.core/defconsumer my-component ctx
  [ctx-value prop1 prop2]
  [:div (prn ctx-value)])
  1. Regarding conversion of CLJS data to JS: This happens anytime you pass in props as an element (e.g. [:div {:on-click #(js/alert "hi"}]), and each time your components are rendered. hx only converts things to and from either shallowly - so for instance, passing in a hugely nested map as a prop won't have any performance penalty. Likewise, any JS data structures passed into your component will be left alone. The perf penalties in this case are not that bad. If you find yourself in a particularly hot-path and need to optimize, you can use JS interop in that one place without affecting the rest of your code.

  2. Regarding using JS data: You can use any CLJS data with all of the hooks. The only place that you have to use raw JS data is when passing in dependencies to things like useEffect; hx.hooks/useEffect et. al. handles that conversion for you. You can certainly store CLJS data inside of useState. Please do!

  3. Regarding naming: good feedback! I'll think about exposing the same functions with descriptive names, as well as adding docstrings to both of them.

Once again, thanks for taking the time to write this up and I hope that we can help aid in things like clearer documentation and easier interop with existing libraries.

DjebbZ commented 5 years ago

@tomconnors I would love some numbers about the code size. Did switching to hx reduce the final bundle size?

tomconnors commented 5 years ago

This is an internal application so I haven't made any effort at all to optimize the build size. Tough to give any useful numbers because I added hx + react-dnd without removing reagent.

DjebbZ commented 5 years ago

Indeed, if you added react-dnd and hx at the same time it's impossible to compare. And you @Lokeh do you have some numbers?

p-himik commented 4 years ago

I don't know where to put it and this seems like the most appropriate place. I tried to incorporate hx components in a Reagent app. Namely, I wanted to add react-beautiful-dnd. I couldn't do it in Reagent alone since table dimension locking requires getSnapshotBeforeUpdate which Reagent doesn't support yet. The example I tried to recreate: https://react-beautiful-dnd.netlify.com/?path=/story/tables--with-dimension-locking

All was well except for table dimensions not being locked. After a fun debug session, it turned out that I have to use react-beautiful-dnd using only hx. I cannot use hx only for the component that requires getSnapshotBeforeUpdate - I have to use it for the whole functionality.

I think the reason behind it is that React and Reagent have separate async rendering machinery. In the working example, getSnapshotBeforeUpdate is fired before the styles of the parent component are updated. And in my version, it was consistently happening after, preventing table cells from locking their dimensions.

lucywang000 commented 4 years ago

@p-himik I think reagent has recently added support for new lifecycle methods like get-snapshot-before-update-test https://github.com/reagent-project/reagent/pull/443