Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade re-frame to latest stable release v1.3.0 #15997

Merged
merged 1 commit into from
May 31, 2023
Merged

Conversation

ilmotta
Copy link
Contributor

@ilmotta ilmotta commented May 23, 2023

Fixes #15963

Summary

This PR upgrades re-frame to v1.3.0 (latest stable release), released ~9 months ago, in 2022-08-27. This is a solid upgrade, with no breaking changes as far as I tested status-mobile. It's a great testament of re-frame's stability and commitment to backwards compatibility, as are many Clojure libs.

The big, and truly relevant addition is the introduction of the :fx built-in effect that was added ~3 years ago in Aug/2020 in v1.1.0. I already did preliminary work on using :fx in status-mobile, but it's not yet ready for a PR (there's more than meets the eye).

Review notes

If we're afraid of causing any disruption this week due to the next mobile release, I think it's wise to merge next week.

Relevant changelog

  • Global interceptors are now supported (added in v1.0.0).
  • reg-event-fx will just warn (not generate an error) if the effect map returned contains an unknown effect key.
  • re-frame will now warn us when we are calling subscribe outside of a reactive context.
  • "re-frame now guarantees that a :db effect, if present, will be actioned before any other sibling effects. re-frame continues to provide NO guarantees about the order in which other effects will be actioned." (https://day8.github.io/re-frame/releases/2020/#110-2020-08-24)
  • There's syntactic sugar for trivial reg-sub declarations (added in v1.3.0). See the documentation for reg-sub for more details https://day8.github.io/re-frame/api-re-frame.core/#reg-sub
  • "The built-in effect :dispatch-later can now take a single map value. Supplying a sequence of maps is now deprecated in favor of using multiple :dispatch-later effects within the new :fx effect." https://day8.github.io/re-frame/releases/2020/#111-2020-08-26

status: ready

@status-im-auto
Copy link
Member

status-im-auto commented May 23, 2023

Jenkins Builds

Click to see older builds (4)
Commit #️⃣ Finished (UTC) Duration Platform Result
✔️ 83c9d35 #1 2023-05-23 14:14:20 ~7 min android-e2e 🤖apk 📲
✔️ 83c9d35 #1 2023-05-23 14:15:10 ~7 min tests 📄log
✔️ 83c9d35 #1 2023-05-23 14:15:19 ~7 min ios 📱ipa 📲
✔️ 83c9d35 #1 2023-05-23 14:15:22 ~7 min android 🤖apk 📲
Commit #️⃣ Finished (UTC) Duration Platform Result
✔️ 04492ab #2 2023-05-24 12:08:00 ~5 min tests 📄log
✔️ 04492ab #2 2023-05-24 12:08:15 ~6 min android 🤖apk 📲
✔️ 04492ab #2 2023-05-24 12:08:28 ~6 min android-e2e 🤖apk 📲
✔️ 04492ab #2 2023-05-24 12:09:31 ~7 min ios 📱ipa 📲
✔️ bc59dca #3 2023-05-31 08:46:08 ~5 min android 🤖apk 📲
✔️ bc59dca #3 2023-05-31 08:46:42 ~5 min android-e2e 🤖apk 📲
✔️ bc59dca #3 2023-05-31 08:47:10 ~6 min ios 📱ipa 📲
✔️ bc59dca #3 2023-05-31 08:49:36 ~8 min tests 📄log

@@ -2,7 +2,7 @@
{:source-paths ["src" "test/cljs"]

:dependencies [[reagent "1.0.0"]
[re-frame "0.12.0"]
[re-frame "1.3.0"]
Copy link
Contributor

@erikseppanen erikseppanen May 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if cider-nrepl, piggieback versions should be updated in shadow-cljs.edn as well...?

...or maybe removed, since I think we're all using local updated versions anyways...?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might not need piggieback anymore: Does shadow-cljs use Piggieback? No, it doesn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if cider-nrepl, piggieback versions should be updated in shadow-cljs.edn as well...?

...or maybe removed, since I think we're all using local updated versions anyways...?

I would prefer if other contributors could share their opinion about that. I would certainly remove cider-nrepl and piggieback, but I'm not sure if it could affect others.

It's worth creating an issue anyway. Thanks for mentioning @erikseppanen

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing cider-nrepl might hurt emacs users. Cider needs the nrepl dep for the editor to jack in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shivekkhurana, I think what @erikseppanen was hinting at is that editor specific deps could be removed from the repo since each developer can use their own preferred versions of these libs, and the ones in the repo are quite outdated anyways. For example, by overwriting deps in ~/.shadow-cljs/config.edn

But for now I'm going to keep these libs in the repo :)

Copy link
Contributor

@erikseppanen erikseppanen Jun 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes thank you @ilmotta, that's what I was referring to. There's a small reference to it in the docs.

I can see some value in maintaining them in shadow-cljs.edn so that a dev wouldn't have to maintain a separate ~/.shadow-cljs/config.edn. But IIRC, the emacs cider packages (for each developer's local emacs installation) need to have matching versions, or you get a warning about packages being out-of-sync.

In general, when I see those outdated deps, I wonder for whose benefit they are there. I guess it doesn't matter that much leaving them in there... it's a low priority.

Copy link
Member

@jakubgs jakubgs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@flexsurfer
Copy link
Member

would be great to bump reagent as well in this PR, wdyt ?

@ilmotta
Copy link
Contributor Author

ilmotta commented May 24, 2023

would be great to bump reagent as well in this PR, wdyt ?

If you don't mind too much, I would prefer for that to be done separately @flexsurfer. I'll merge this one today (yesterday).

Edit: Waiting for this week's mobile release.

@status-im-auto
Copy link
Member

94% of end-end tests have passed

Total executed tests: 33
Failed tests: 2
Passed tests: 31
IDs of failed tests: 702782,702838 

Failed tests (2)

Click to expand
  • Rerun failed tests

  • Class TestCommunityMultipleDeviceMerged:

    1. test_community_message_send_check_timestamps_sender_username, id: 702838

    Device 2: Verifying that 'hello' is under today
    Device 2: Looking for a message by text: hello

    critical/test_public_chat_browsing.py:456: in test_community_message_send_check_timestamps_sender_username
        channel.verify_message_is_under_today_text(message, self.errors)
    ../views/chat_view.py:944: in verify_message_is_under_today_text
        message_element.wait_for_visibility_of_element()
    ../views/base_element.py:135: in wait_for_visibility_of_element
        raise TimeoutException(
     Device 2: ChatElementByText by xpath:`//*[starts-with(@text,'hello')]/ancestor::android.view.ViewGroup[@content-desc='chat-item']` is not found on the screen after wait_for_visibility_of_element 
    

    [[blocked by 14797]]

    Device sessions

    Class TestOneToOneChatMultipleSharedDevicesNewUi:

    1. test_1_1_chat_emoji_send_reply_and_open_link, id: 702782

    Device 1: Tap on found: SendMessageButton
    Device 2: Find Button by xpath: //*[starts-with(@text,'http://status.im')]

    critical/chats/test_1_1_public_chats.py:930: in test_1_1_chat_emoji_send_reply_and_open_link
        self.chat_2.element_starts_with_text(url_message, 'button').click_inside_element_by_coordinate(0.2, 0.5)
    ../views/base_element.py:328: in click_inside_element_by_coordinate
        location, size = self.get_element_coordinates()
    ../views/base_element.py:266: in get_element_coordinates
        element = self.find_element()
    ../views/base_element.py:80: in find_element
        raise NoSuchElementException(
     Device 2: Button by xpath: `//*[starts-with(@text,'http://status.im')]` is not found on the screen
    



    Device sessions

    Passed tests (31)

    Click to expand

    Class TestCommunityOneDeviceMerged:

    1. test_restore_multiaccount_with_waku_backup_remove_switch, id: 703133
    Device sessions

    2. test_community_navigate_to_channel_when_relaunch, id: 702846
    Device sessions

    3. test_community_copy_and_paste_message_in_chat_input, id: 702742
    Device sessions

    Class TestGroupChatMultipleDeviceMergedNewUI:

    1. test_group_chat_pin_messages, id: 702732
    Device sessions

    2. test_group_chat_join_send_text_messages_push, id: 702807
    Device sessions

    3. test_group_chat_offline_pn, id: 702808
    Device sessions

    Class TestCommunityMultipleDeviceMerged:

    1. test_community_several_images_send_reply, id: 703194
    Device sessions

    2. test_community_message_delete, id: 702839
    Device sessions

    3. test_community_contact_block_unblock_offline, id: 702894
    Device sessions

    4. test_community_leave, id: 702845
    Device sessions

    5. test_community_one_image_send_reply, id: 702859
    Device sessions

    6. test_community_message_edit, id: 702843
    Device sessions

    7. test_community_mentions_push_notification, id: 702786
    Device sessions

    8. test_community_mark_all_messages_as_read, id: 703086
    Device sessions

    9. test_community_links_with_previews_github_youtube_twitter_gif_send_enable, id: 702844
    Device sessions

    10. test_community_unread_messages_badge, id: 702841
    Device sessions

    11. test_community_emoji_send_copy_paste_reply, id: 702840
    Device sessions

    Class TestOneToOneChatMultipleSharedDevicesNewUi:

    1. test_1_1_chat_push_emoji, id: 702813
    Device sessions

    2. test_1_1_chat_is_shown_message_sent_delivered_from_offline, id: 702783
    Device sessions

    3. test_1_1_chat_text_message_delete_push_disappear, id: 702733
    Device sessions

    4. test_1_1_chat_message_reaction, id: 702730
    Device sessions

    5. test_1_1_chat_edit_message, id: 702855
    Device sessions

    6. test_1_1_chat_pin_messages, id: 702731
    Device sessions

    7. test_1_1_chat_delete_via_long_press_relogin, id: 702784
    Device sessions

    8. test_1_1_chat_non_latin_messages_stack_update_profile_photo, id: 702745
    Device sessions

    Class TestActivityCenterContactRequestMultipleDevicePR:

    1. test_activity_center_contact_request_accept_swipe_mark_all_as_read, id: 702851
    Device sessions

    2. test_activity_center_contact_request_decline, id: 702850
    Device sessions

    Class TestActivityMultipleDevicePR:

    1. test_activity_center_reply_read_unread_delete_filter_swipe, id: 702947
    Device sessions

    2. test_navigation_jump_to, id: 702936
    Device sessions

    3. test_activity_center_mentions, id: 702957
    Device sessions

    4. test_activity_center_admin_notification_accept_swipe, id: 702958
    Device sessions

    @ilmotta ilmotta merged commit 9dad471 into develop May 31, 2023
    @ilmotta ilmotta deleted the upgrade-re-frame branch May 31, 2023 09:08
    ilmotta added a commit that referenced this pull request Oct 5, 2023
    This commit shows how to move away from rf/defn
    https://github.com/status-im/status-mobile/blob/f12c7401d167d7adb81f97bdf9c0902b39ee37bc/src/utils/re_frame.clj#L1-L90
    & rf/merge
    https://github.com/status-im/status-mobile/blob/f12c7401d167d7adb81f97bdf9c0902b39ee37bc/src/utils/re_frame.cljs#L39-L85
    and why we should do it.
    
    ## Problems
    
    Before jumping to solutions, let's understand the problems first, in no order of
    importance.
    
    ### Problem 1: Cyclic dependencies
    
    If you ever tried to move event handlers or the functions used inside them to
    different files in status-mobile, you probably stumbled in cyclic dependency
    errors.
    
    When an event is registered in re-frame, it is globally available for any other
    place to dispatch. The dispatch mechanism relies on globally unique keywords,
    the so called event IDs, e.g. :chat/mute-successfully. This means that event
    namespaces don't need to require other event namespaces, just like you don't
    need to require subscription namespaces in views.
    
    rf/merge increases the likelihood of cyclic dependencies because they force
    event namespaces to require each other. Although not as common, this happened a
    few times with devs in the team and it can be a big hassle to fix if you are
    unlucky. It is a problem we should not have in the first place (at least not as
    easily).
    
    ### Problem 2: We are not linting thousands of lines of code
    
    The linter (clj-kondo) is incapable of understanding the rf/defn macro. In
    theory, we could teach clj-kondo what the macro produces. I tried this, but gave
    up after a few tries.
    
    This is a big deal, clj-kondo can catch many issues and will continue to catch
    more as it continue to evolve. It's hard to precisely count how many lines are
    affected, but `find src/ -type f -name 'events.cljs' -exec wc -l {} +` gives us
    more than 4k LOC.
    
    ### Problem 3: Blocking RN's UI thread for too long
    
    Re-frame has a routing mechanism to manage events. When an event is dispatched,
    it is enqueued and scheduled to run some time later (very soon). This process is
    asynchronous and is optimized in such a way as to balance responsiveness vs the
    time to empty the queue.
    
    >[...] when processing events, one after the other, do ALL the currently queued
    >events. Don't stop. Don't yield to the browser. Hog that CPU.
    >
    >[...] but if any new events are dispatched during this cycle of processing,
    >don't do them immediately. Leave them queued.
    >
    >-- https://github.com/day8/re-frame/blob/master/src/re_frame/router.cljc#L8-L60
    
    Decisions were made (way back in 2017) to reduce the number of registered
    re-frame events and, more importantly, to coalesce events into bigger ones with
    the rf/merge pattern. I tried to find evidence of real problems that were trying
    to be solved, but my understanding is that decisions were largely based on
    personal architectural preferences.
    
    Fast-forward to 2023, and we are in a situation where we have many heavy events
    that process a LOT of stuff in one go using rf/merge, thus blocking the UI
    thread longer than we should. See, for example,
    [status-im2.contexts.profile.login.events/login-existing-profile](https://github.com/status-im/status-mobile/blob/3082605d1e9897da1b1784588aa22fdc65c84823/src/status_im2/contexts/profile/login/events.cljs#L69),
    [status-im2.contexts.profile.login.events/get-chats-callback](https://github.com/status-im/status-mobile/blob/3082605d1e9897da1b1784588aa22fdc65c84823/src/status_im2/contexts/profile/login/events.cljs#L98),
    and many others.
    
    The following excerpt was generally used to justify the idea that coalescing
    events would make the app perform better.
    
    > We will reduce the the amount of subscription re-computations, as for each
    > distinct action, :db effect will be produced and swapped into app-db only once
    >
    > -- status-im/swarms#31 (comment)
    
    This is in fact incorrect. Re-frame, ever since 2015 (so before the original
    discussions in 2017) uses a concept of batching to process events, which means
    subscriptions won't re-run after every dispatched event, and thus components
    won't re-render either. Re-frame is smarter than that.
    
    > groups of events queued up will be handled in a batch, one after the other,
    > without yielding to the browser (previously re-frame yielded to the browser
    > before every single event).
    >
    > -- https://github.com/day8/re-frame/blob/39adca93673f334dc751ee2d99d340b51a9cc6db/docs/releases/2015.md#050--2015-11-5
    
    Here's a practical example you can try in a shadow-cljs :mobile REPL to see the
    batching behavior in practice.
    
    ```clojure
    ;; A dummy event that toggles between DEBUG and INFO levels.
    (re-frame/reg-event-fx :dummy-event
      (fn [{:keys [db]}]
        {:db (update-in db
                        [:profile/profile :log-level]
                        (fn [level]
                          (if (= "DEBUG" level)
                            "INFO"
                            "DEBUG")))}))
    
    (def timer
      (js/setInterval #(re-frame/dispatch [:dummy-event])
                      50))
    
    ;; 1. In component status-im.ui.screens.advanced-settings.views/advanced-settings,
    ;; add a print call to see when it's re-rendered by Reagent because the
    ;; subscription :log-level/current-log-level will be affected by our dummy event.
    ;;
    ;; 2. Also add a print call to the subscription :log-level/current-log-level to
    ;; see that the subscription does NOT re-run on every dispatch.
    
    ;; Remember to eval this expression to cancel the timer.
    (js/clearInterval timer)
    ```
    
    If you run the above timer with 50ms interval, you'll see a fair amount of
    batching happening. You can infer that's the case because you'll see way less
    than 20 print statements per second (so way less than 20 recomputations of the
    subscription, which is the max theoretical limit).
    
    When the interval is reduced even more, to say 10ms (to simulate lots of
    dispatches in a row), sometimes you don't see a single recomputation in a 5s
    window because re-frame is too busy processing events.
    
    This shows just how critical it is to have event handlers finishing as soon as
    possible to relinquish control back to the UI thread, otherwise responsiveness
    is affected. It also shows that too many dispatches in a row can be bad, just as
    big event handlers would block the batch for too long. You see here that
    dispatching events in succession does NOT cause needless re-computations.
    
    Of course there's an overhead of using re-frame.core/dispatch instead of calling
    a Clojure function, but the trade-off is clearly documented: the more we
    process in a single event, the less responsive the app may be because re-frame
    won't be able to relinquish control back to the UI thread. The total time to
    process the batch increases, but re-frame can't stop in the middle compared to
    when different dispatches are used.
    
    Thus, I believe this rf/merge pattern is harmful as a default practice in an
    environment such as ours, where it's desirable end-users feel a snappy RN app. I
    actually firmly believe we can improve the app's responsiveness by not
    coalescing events by default. We're also preventing status-mobile from taking
    the most advantage from future improvements in re-frame's scheduler. I can
    totally see us experimenting with other algorithms in the scheduler to best fit
    our needs. We should not blindly reduce the number of events as stated here
    #2634 (comment).
    
    Solution: only coalesce events into one pile when it's strictly desirable to
    atomically update the app db to avoid inconsistencies, otherwise, let the
    re-frame scheduler do its job by using fx, not rf/merge. When needed, embrace
    *eventual app db consistency* as a way to achieve lower UI latency, i.e. write
    fast and short events, intentionally use :dispatch-later or other timing effects
    to bend the re-frame's scheduler to your will.
    
    There's another argument in favor of using something like rf/merge which I would
    like to deconstruct. rf/merge gives us a way to reuse computations from
    different events, which is nice. The thing here is that we don't need rf/merge
    or re-frame to reuse functions across namespaces. rf/merge complects re-frame
    with the need to reuse transformations.
    
    Instead, the solution is as trivial as it gets, reuse app db "transformers"
    across events by extracting the logic to data store namespaces
    (src/status_im/data_store). This solution has the added benefit of not causing
    cyclic dependency errors.
    
    ### Problem 4: Clojure's language server doesn't understand code under rf/defn
    
    Nowadays, many (if not most) Clojure devs rely on the Clojure Language Server
    https://github.com/clojure-lsp/clojure-lsp to be more effective. It is an
    invaluable tool, but it doesn't work well with the macro rf/defn, and it's a
    constant source of frustration when working in event namespaces. Renaming
    symbols inside the macro don't work, finding references, jumping to local
    bindings, etc.
    
    Solution: don't use rf/defn, instead use re-frame's reg-event-fx function and
    clojure-lsp will understand all the code inside event handlers.
    
    ### Problem 5: Unit tests for events need to "test the world"
    
    Re-frame's author strongly recommends testing events that contain non-trivial
    data transformations, and we do have many in status-mobile (note: let's not
    confuse with integration tests in status_im/integration_test.cljs). That, and
    non-trivial layer-3 subscriptions should be covered too. The reasoning is that
    if we have a well developed and tested state layer, many UI bugs can be
    prevented as the software evolves, since the UI is partially or greatly derived
    from the global state. See re-frame: What to Test?
    https://github.com/day8/re-frame/blob/39adca93673f334dc751ee2d99d340b51a9cc6db/docs/Testing.md#what-to-test.
    See PR Introduce subscription tests
    #14472, where I share more
    details about re-frame's testing practices.
    
    When we use rf/merge, we make unit testing events a perennial source of
    frustration because too many responsibilities are aggregated in a single event.
    Unfortunately, we don't have many devs in the team that attempted to write unit
    tests for events to confirm my claim, but no worries, let's dive into a real
    example.
    
    In a unit test for an event, we want to test that, given a cofx and args, the
    event handler returns the expected map of effects with the correct values
    (usually db transformations).
    
    Let's assume we need to test the following event. The code is still using the
    combo rf/defn & rf/merge.
    
    ```clojure
    (rf/defn accept-notification-success
      {:events [:activity-center.notifications/accept-success]}
      [{:keys [db] :as cofx} notification-id {:keys [chats]}]
      (when-let [notification (get-notification db notification-id)]
        (rf/merge cofx
                  (chat.events/ensure-chats (map data-store.chats/<-rpc chats))
                  (notifications-reconcile [(assoc notification :read true :accepted true)]))))
    ```
    
    As you can see, we're "rf/merging" two other functions, namely ensure-chats and
    notifications-reconcile. In fact, ensure-chats is not registered in re-frame,
    but it's 99% defined as if it's one because it needs to be "mergeable" according
    to the rules of rf/merge. Both of these "events" are quite complicated under the
    hood and should be unit tested on their own.
    
    Now here goes the unit test. Don't worry about the details, except for the expected output.
    
    ```clojure
    (deftest accept-notification-success-test
      (testing "marks notification as accepted and read, then reconciles"
        (let [notif-1          {:id "0x1" :type types/private-group-chat}
              notif-2          {:id "0x2" :type types/private-group-chat}
              notif-2-accepted (assoc notif-2 :accepted true :read true)
              cofx             {:db {:activity-center {:filter        {:type types/no-type :status :all}
                                                       :notifications [notif-2 notif-1]}}}
    
              expected {:db         {:activity-center {:filter        {:type 0 :status :all}
                                                       :notifications [notif-2-accepted notif-1]}
                                     :chats           {}
                                     :chats-home-list nil}
                        ;; *** HERE ***
                        :dispatch-n [[:activity-center.notifications/fetch-unread-count]
                                     [:activity-center.notifications/fetch-pending-contact-requests]]}
              actual   (events/accept-notification-success cofx (:id notif-2) nil)]
          (is (= expected actual)))))
    ```
    
    Notice the map has a :dispatch-n effect and other stuff inside of it that are
    not the responsibility of the event under test to care about. This happens
    because rf/merge forces the event handler to compute/call everything in one go.
    And things get MUCH worse when you want to test an event A that uses rf/merge,
    but A calls other events B and C that also use rf/merge (e.g. event
    :profile.login/get-chats-callback). At that point you flip the table in horror
    😱, but testing events and maintaining them should be trivial.
    
    Solution: Use re-frame's `fx` effect.
    
    Here's the improved implementation and its accompanying test.
    
    ```clojure
    (defn accept-notification-success
      [{:keys [db]} [notification-id {:keys [chats]}]]
      (when-let [notification (get-notification db notification-id)]
        (let [new-notifications [(assoc notification :read true :accepted true)]]
          {:fx [[:dispatch [:chat/ensure-chats (map data-store.chats/<-rpc chats)]]
                [:dispatch [:activity-center.notifications/reconcile new-notifications]]]})))
    
    (re-frame/reg-event-fx :activity-center.notifications/accept-success accept-notification-success)
    
    (deftest accept-notification-success-test
      (testing "marks notification as accepted and read, then reconciles"
        (let [notif-1          {:id "0x1" :type types/private-group-chat}
              notif-2          {:id "0x2" :type types/private-group-chat}
              notif-2-accepted (assoc notif-2 :accepted true :read true)
              cofx             {:db {:activity-center {:filter        {:type types/no-type :status :all}
                                                       :notifications [notif-2 notif-1]}}}
    
              ;; *** HERE ***
              expected {:fx [[:dispatch [:chat/ensure-chats []]]
                             [:dispatch [:activity-center.notifications/reconcile [notif-2-accepted]]]]}
              actual   (events/accept-notification-success cofx [(:id notif-2) nil])]
          (is (= expected actual)))))
    ```
    
    Notice how the test expectation is NOT verifying what other events do (it's
    actually "impossible" now). Using fx completely decouples events and makes
    testing them a joy again.
    
    ### Problem 6: Unordered effects
    
    status-mobile still uses the legacy way to describe the effects map, which has
    the problem that their order is unpredictable.
    
    > Prior to v1.1.0, the answer is: no guarantees were provided about ordering.
    > Actual order is an implementation detail upon which you should not rely.
    >
    > -- https://github.com/day8/re-frame/blob/39adca93673f334dc751ee2d99d340b51a9cc6db/docs/Effects.md#order-of-effects
    
    > In fact, with v1.1.0 best practice changed to event handlers should only
    > return two effects :db and :fx, in which case :db was always done first and
    > then :fx, and within :fx the ordering is sequential. This new approach is more
    > about making it easier to compose event handlers from many smaller functions,
    > but more specificity around ordering was a consequence.
    >
    > -- https://github.com/day8/re-frame/blob/39adca93673f334dc751ee2d99d340b51a9cc6db/docs/Effects.md#order-of-effects
    
    ### Problem 7: Usage of deprecated effect dispatch-n
    
    We have 35 usages, the majority in new code using dispatch-n, which has been
    officially deprecated in favor of multiple dispatch tuples in fx. See
    https://github.com/day8/re-frame/blob/39adca93673f334dc751ee2d99d340b51a9cc6db/docs/api-builtin-effects.md#L114
    
    ### Problem 8: Complexity 🧙‍♂️
    
    Have you ever tried to understand and/or explain how rf/merge and rf/defn work?
    They have their fare share of complexity and have tripped up many contributors.
    
    This is not ideal if we want to create a project where contributors can learn
    re-frame as quickly as possible. Re-frame is already complicated enough to grasp
    for many, the added abstractions should be valuable enough to justify.
    
    Interestingly, rf/merge is a stateful function, and although this is not a
    problem in practice, it is partially violating re-frame's spirit of only using
    pure functions inside event handlers.
    
    ### Problem 9: Using a wrapping macro rf/defn instead of global interceptors
    
    When rf/defn was created inside status-mobile, re-frame didn't have global
    interceptors yet (which were introduced 3+ years ago). We no longer have this
    limitation after we upgraded our old re-frame version in PR
    #15997.
    
    Global interceptors are a simple and functional abstraction to specify functions
    that should run on every event, for example, for debugging during development,
    logging, etc. This PR already shows this is possible by removing the wrapping
    function utils.re-frame/register-handler-fx without causing any breakage.
    
    ## Conclusion
    
    By embracing re-frame's best practices for describing effects
    https://github.com/day8/re-frame/blob/39adca93673f334dc751ee2d99d340b51a9cc6db/docs/FAQs/BestPractice.md#use-the-fx-effect,
    we can solve long standing issues that affect every contributor at different
    levels and bring the following benefits:
    
    - Simplify the codebase.
    - Bring back the DX we all deserve, i.e. Clojure Language Server and clj-kondo
      fully working in event namespaces.
    - Greatly facilitate the testability of events.
    - Give devs more flexibility to make the app more responsive, because the new
      default would not coalesce events, which in turn, would block the UI thread
      for shorter periods of time. At least that's the theory, but exceptions will
      be found.
    
    The actions to achieve those benefits are:
    
    - Don't use the macro approach, replace rf/defn with
      re-frame.core/reg-event-fx.
    - Don't use rf/merge, simply use re-frame's built-in effect :fx.
    - Don't call event handlers as normal functions, just as we don't directly call
      subscription handlers. Use re-frame's built-in effect :fx.
    
    ## How do we refactor the remainder of the code?
    
    Some numbers first:
    
    - There are 228 events defined with rf/defn in src/status-im2/.
    - There are 34 usages of rf/merge in src/status_im2/.
    
    ## Resources
    
    - Release notes where fx was introduced in re-frame:
      https://github.com/day8/re-frame/blob/39adca93673f334dc751ee2d99d340b51a9cc6db/docs/releases/2020.md#110-2020-08-24
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    None yet
    Projects
    No open projects
    Archived in project
    Development

    Successfully merging this pull request may close these issues.

    Upgrade re-frame
    9 participants