Compare commits

...
Author SHA1 Message Date
Icaro Motta e1a868fc0d Experiment with more solutions for using stable handlers
Re-frame docs/on-stable-dom-handlers.md offers a clever solution to cache any
function even if it needs to "close over" dynamic values.

In this commit there are two solutions, one that's independent of React, i.e. a
pure Clojure solution that should be used only with form-2 components and a
second one, that uses hooks and persists state using React refs.

Both solutions are capable of solving the problem of stable handlers. The
solution using a form-2 component is committed here as an implementation
reference since it was taken from re-frame docs. The hooks solution should be
battle tested in more components to make sure it's good enough.
2024-05-20 14:54:10 -03:00
Sean Hagstrom 4d3a564312 tidy: focus on the ergonomics of using use-bind-snapshot 2024-05-20 14:54:10 -03:00
Sean Hagstrom 757b8ebcac tweak: utilise use-layout-effect to safely update atom 2024-05-20 14:54:10 -03:00
Sean Hagstrom 5ff9097410 tidy: refactor from use-callback to use-memo 2024-05-20 14:54:10 -03:00
Sean Hagstrom 6fdb57a753 chore: add use-layout-effect 2024-05-20 14:54:10 -03:00
Sean Hagstrom 8d821c27cb tidy: remove extra debug labels 2024-05-20 14:54:10 -03:00
Sean Hagstrom 3be9bd0bbb wip: refactor to use custom memo implementation and display demo of different behaviors 2024-05-20 14:54:10 -03:00
Sean Hagstrom 541e51b37b tweak: test sharing a state-sub 2024-05-20 14:54:10 -03:00
Sean Hagstrom c3a62f080b tidy: remove unused button stuff 2024-05-20 14:54:10 -03:00
Sean Hagstrom 5346dfec0f tweak: update demo with a variety of helper functions 2024-05-20 14:54:10 -03:00
Sean Hagstrom 83e9f8f0fc tweak: use debug labels for buttons 2024-05-20 14:54:10 -03:00
Sean Hagstrom 691768b691 tweak: only debug buttons with string children 2024-05-20 14:54:10 -03:00
Sean Hagstrom 797578ae73 tidy: rename functions to be more descriptive 2024-05-20 14:54:09 -03:00
Sean Hagstrom 5f7081c30f fix: subscription leak during hot reload by using use-memo 2024-05-20 14:54:09 -03:00
Sean Hagstrom 8efbf7e43d tweak: log when button component renders 2024-05-20 14:54:09 -03:00
Sean Hagstrom 97019fffee tweak: refactor example to use reagent.ratom/track and reagent.ratom/track! functions with message state 2024-05-20 14:54:09 -03:00
Sean Hagstrom 143410871b wip: add small experiment for ui callback with state and memoization 2024-05-20 14:54:09 -03:00
Sean Hagstrom 85382bbc28 tweak: forward on-press events 2024-05-20 14:54:09 -03:00
4 changed files with 210 additions and 19 deletions
@@ -35,6 +35,7 @@
size 40
customization-color (if (= type :primary) :blue nil)}}
children]
(when (string? children) (js/console.log "button - " children))
(let [[pressed-state? set-pressed-state] (rn/use-state false)
theme (quo.theme/use-theme)
{:keys [icon-color background-color label-color border-color blur-type
+37
View File
@@ -181,6 +181,14 @@
#(let [ret (handler)] (if (fn? ret) ret js/undefined))
(get-js-deps deps))))
(defn use-layout-effect
([handler]
(use-layout-effect handler nil))
([handler deps]
(react/useLayoutEffect
#(let [ret (handler)] (if (fn? ret) ret js/undefined))
(get-js-deps deps))))
(defn use-mount
[handler]
(use-effect handler []))
@@ -195,6 +203,35 @@
([handler deps]
(react/useCallback handler (get-js-deps deps))))
(defn use-fn-factory-factory
"A dirty code translation from the form-2 solution `make-fn-factory`."
[f]
(let [*args1 (use-ref-atom nil)
cached (use-ref-atom
(fn [& args2]
(apply f (concat @*args1 args2))))
factory (use-ref-atom
(fn factory
[& args1]
(reset! *args1 args1)
@cached))]
@factory))
(defn use-fn-factory
[f & args]
(let [factory (use-fn-factory-factory f)]
(apply factory args)))
(defn make-fn-factory
"Should be used in a form-2 component."
[f]
(let [*args1 (atom nil)
cached (fn [& args2]
(apply f (concat @*args1 args2)))]
(fn [& args1]
(reset! *args1 args1)
cached)))
(defn use-memo
[handler deps]
(react/useMemo handler (get-js-deps deps)))
+2 -2
View File
@@ -14,11 +14,11 @@
[{:keys [on-press allow-multiple-presses? throttle-duration]} throttle-id]
(if allow-multiple-presses?
on-press
(fn []
(fn [event]
(let [id @throttle-id]
(when (and id (not (get @throttle id)))
(swap! throttle assoc id true)
(on-press)
(on-press event)
(js/setTimeout
#(swap! throttle dissoc id)
(or throttle-duration 500)))))))
@@ -3,32 +3,173 @@
[quo.core :as quo]
[react-native.core :as rn]
[react-native.platform :as platform]
[reagent.ratom]
[status-im.constants :as constants]
[status-im.contexts.profile.contact.contact-request.style :as style]
[status-im.contexts.profile.utils :as profile.utils]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn view
;; ---
(def ^:private lookup-sentinel (js-obj))
(defn memo
"Returns a memoized version of a referentially transparent function. The
memoized version of the function keeps a cache of the mapping from arguments
to results and, when calls with the same arguments are repeated often, has
higher performance at the expense of higher memory use."
[mem f]
(fn [& args]
(js/console.log "lookup")
(let [v (get @mem args lookup-sentinel)]
(if (identical? v lookup-sentinel)
(let [ret (apply f args)]
(js/console.log "swap")
(swap! mem assoc args ret)
ret)
v))))
(defn make-snapshot-handler-factory
[snapshot]
(let [storage (rn/use-memo #(atom {}) [])
capture (rn/use-memo #(atom snapshot) [])
_update (rn/use-layout-effect
(fn []
(swap! capture (fn [_] snapshot)))
snapshot)
factory (rn/use-memo
(fn []
(fn [handler]
((memo storage
(fn [callback]
(fn [event]
(callback @capture event))))
handler)))
[storage capture])]
{:factory factory
:storage storage}))
(defn use-bind-snapshot
[snapshot]
(let [{:keys [factory storage]}
(make-snapshot-handler-factory snapshot)]
(rn/use-unmount (fn []
(js/console.log "unmount bind-snapshot")
(prn "unmount storage" storage)))
factory))
;; ---
(defn on-message-submit
[public-key message _event]
(rf/dispatch [:hide-bottom-sheet])
(rf/dispatch [:contact.ui/send-contact-request public-key message])
(rf/dispatch [:toasts/upsert
{:id :send-contact-request
:type :positive
:text (i18n/label
:t/contact-request-was-sent)}]))
(defn on-message-change
[set-message full-name message-text]
;; This is just an example tap to check that when full-name is recomputed,
;; this function sees the most up-to-date value.
(tap> [:on-message-change full-name message-text])
(set-message message-text))
(defn use-event
[f scope]
(let [bind (use-bind-snapshot scope)]
(bind f)))
(defn use-events
[fs scope]
(let [bind (use-bind-snapshot scope)]
(->> fs
(reduce-kv
(fn [acc label f]
(assoc acc label (bind f)))
{}))))
(defn view--using-factory-with-form-2
[]
(let [on-change-factory (rn/make-fn-factory on-message-change)
on-submit-factory (rn/make-fn-factory on-message-submit)]
(fn []
(let [{:keys [public-key customization-color]
:as profile} (rf/sub [:contacts/current-contact])
full-name (profile.utils/displayed-name profile)
profile-picture (profile.utils/photo profile)
input-ref (rn/use-ref-atom nil)
[message set-message] (rn/use-state "")
on-change (on-change-factory set-message full-name)
on-submit (on-submit-factory public-key message)]
(rn/use-mount
(fn []
(let [listener (.addListener rn/keyboard
"keyboardDidHide"
(fn [_event]
(when (and platform/android? @input-ref)
(.blur ^js @input-ref))))]
#(.remove ^js listener))))
[:<>
[quo/drawer-top
{:type :context-tag
:context-tag-type :default
:title (i18n/label :t/send-contact-request)
:full-name full-name
:profile-picture profile-picture
:customization-color customization-color}]
[quo/text {:style style/message-prompt-wrapper}
(i18n/label :t/contact-request-message-prompt)]
[rn/view {:style style/message-input-wrapper}
[quo/input
{:type :text
:ref #(reset! input-ref %)
:multiline? true
:char-limit constants/contact-request-message-max-length
:max-length constants/contact-request-message-max-length
:placeholder (i18n/label :t/type-something)
:auto-focus true
:accessibility-label :contact-request-message
:label (i18n/label :t/message)
:on-change-text on-change
:container-style {:flex-shrink 1}
:input-container-style {:flex-shrink 1}}]]
[quo/bottom-actions
{:container-style {:style {:flex 1}}
:actions :one-action
:button-one-props {:disabled? (string/blank? message)
:accessibility-label :send-contact-request
:customization-color :purple
:on-press on-submit}
:button-one-label "Past Present Sub"}]
[quo/bottom-actions
{:container-style {:style {:flex 1}}
:actions :two-actions
:button-one-props {:accessibility-label :send-contact-request
:customization-color :blue
:on-press on-submit}
:button-one-label "Snapshot"
:button-two-props {:accessibility-label :test-button
:customization-color :orange
:on-press on-submit}
:button-two-label "Sub"}]]))))
(defn view--using-factory-hook
[]
(let [{:keys [public-key customization-color]
:as profile} (rf/sub [:contacts/current-contact])
customization-color customization-color
full-name (profile.utils/displayed-name profile)
profile-picture (profile.utils/photo profile)
input-ref (rn/use-ref-atom nil)
[message set-message] (rn/use-state "")
on-message-change (rn/use-callback #(set-message %))
on-message-submit (rn/use-callback (fn []
(rf/dispatch [:hide-bottom-sheet])
(rf/dispatch [:contact.ui/send-contact-request
public-key message])
(rf/dispatch [:toasts/upsert
{:id :send-contact-request
:type :positive
:text (i18n/label
:t/contact-request-was-sent)}]))
[public-key message])]
on-change (rn/use-fn-factory on-message-change set-message full-name)
on-submit (rn/use-fn-factory on-message-submit public-key message)]
;; `on-change` is always the same function instance, even if the arguments
;; `set-message` and `full-name` change.
(tap> {:hashes {:on-change (hash on-change)}})
(rn/use-mount
(fn []
(let [listener (.addListener rn/keyboard
@@ -58,7 +199,7 @@
:auto-focus true
:accessibility-label :contact-request-message
:label (i18n/label :t/message)
:on-change-text on-message-change
:on-change-text on-change
:container-style {:flex-shrink 1}
:input-container-style {:flex-shrink 1}}]]
[quo/bottom-actions
@@ -66,7 +207,19 @@
:actions :one-action
:button-one-props {:disabled? (string/blank? message)
:accessibility-label :send-contact-request
:customization-color customization-color
:on-press on-message-submit}
:button-one-label (i18n/label :t/send-contact-request)}]]))
:customization-color :purple
:on-press on-submit}
:button-one-label "Past Present Sub"}]
[quo/bottom-actions
{:container-style {:style {:flex 1}}
:actions :two-actions
:button-one-props {:accessibility-label :send-contact-request
:customization-color :blue
:on-press on-submit}
:button-one-label "Snapshot"
:button-two-props {:accessibility-label :test-button
:customization-color :orange
:on-press on-submit}
:button-two-label "Sub"}]]))
(def view view--using-factory-hook)