nosco / hx

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

track dependencies to selectively rerender with useMemo #65

Open alidcast opened 4 years ago

alidcast commented 4 years ago

Hey @Lokeh JS dev here, just started learning Clojure. I appreciate your work in integrating JS/react tools, and just now read your "when-is-immutability-fast" article.

Regarding your "wrapping all of our components in React.memo" comment in the article, I also thought about this when learning about macros. I like the idea of taking advantage of Clojure so that we can stop thinking about performance and focus on the UI. The concern seems to be that not every component needs this optimization - but wouldn't it be possible to track dependencies, and only apply the useMemo hook where it's necessary?

JS tools like mobx provide this rendering magic via proxies, but since macros give control over code+data this optimization could be done via react itself without having to add another dependency.

Just started learning Clojure/macros of course, so this is all high level for me still, would be great to hear your thoughts! Thanks

lilactown commented 4 years ago

The primary problem is still knowing whether the performance impact of memoizing the calculation is less than the performance impact of running the render function again.

One thing I do have on an alpha branch are some macros that will automatically detect what dependencies to add to the array we give to React's useMemo / useEffect / etc. Something like:

(use-memo
  [foo] ;; <-- normal use, passing in a vector of deps to give to React for memoizing the calculation
  (+ foo 1))

(use-memo
  :auto-deps ;; <-- tells macro to detect and add dependencies in the calculation automatically
  (+ foo 1)) ;; <-- macro will detect that `foo` is used in this and add it to the deps passed to React

Re: mobx. Mobx works similar to Reagent, and the way they are able to optimize renders is by tracking state outside of the render tree. The same thing can be done with hx, but it comes with some drawbacks. The pure React way focuses on keeping state within the render tree and aggressively annotating our components with memo when needed, I don't think hx can get around that yet.

alidcast commented 4 years ago

I see, thanks for clarifying.

the biggest issue imo is not one-off declarations / nesting props but trying to use Context API for global state management, since all consuming components render for on any state change, regardless of whether their using the state. and it'd be great not to have to always use use-memo when consuming a context provider

maybe like the use-memo :auto-deps, there can also be a create-context utility for overcoming the above issue. react-tracked and constate are examples I've been looking at

lilactown commented 4 years ago

I've thought about this some more and I like what you're thinking about re: Context.

I think that this should still be opt-in, but can be made much more ergonomic with some syntax sugar.

What I'm currently playing with in my head is annotating expressions with metadata, which the defnc macro could then inspect and expand into a call to use-memo or use-callback. For instance:

(defnc MyComponent []
  (let [state (hooks/useContext global-state-context)
          foo (:foo state)]
    ^:memo [:div foo]))

Which would be expanded to:

(defnc MyComponent []
  (let [state (hooks/useContext global-state-context)
          foo (:foo state)]
    (hooks/useMemo
      (fn [] [:div foo])
      [foo])))