Timeout Effect in Re-frame

Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.

Sometimes you need to delay an Event a certain amount of time. Let's say you want an alert to appear for twenty seconds before disappearing. We can store the alert message in the Database, but how do we make it disappear after the right time? We could set a timeout, using setTimeout(), but Event handlers should be pure to make them more testable. We need to turn setting a timeout into an Effect.

Let's make the shell of an Effect:

(rf/reg-fx
  :timeout
  (fn [{}]
    ))

We can pass in a time and an Event to dispatch. We then call setTimeout().

(rf/reg-fx
  :timeout
  (fn [{:keys [event time]}]
    (js/setTimeout
      (fn []
        (rf/dispatch event))
      time)))

That's great! Another feature of the JavaScript timeouts is the ability to cancel them with clearTimeout(). setTimeout() will return an id, which we later pass to clearTimeout().

We cancel timeouts if we store the id somewhere. Let's make a new atom to store these ids.

(defonce timeouts (reagent/atom {}))

We can use our own id when we create the Event so we can refer to it later:

(rf/reg-fx
  :timeout
  (fn [{:keys [id event time]}]

If we already have a timeout for this id, let's cancel it:

(rf/reg-fx
  :timeout
  (fn [{:keys [id event time]}]
    (when-some [existing (get @timeouts id)]
      (js/clearTimeout existing))

Then, we dissoc it from the timeouts atom.

(rf/reg-fx
  :timeout
  (fn [{:keys [id event time]}]
    (when-some [existing (get @timeouts id)]
      (js/clearTimeout existing)
      (swap! timeouts dissoc id))
    (when (some? event)
      (swap! timeouts assoc id
        (js/setTimeout
          (fn []
            (rf/dispatch event))
          time)))))

There! Now we can use it like the following. We can set the alert message and trigger its removal after 20 seconds.

(rf/reg-event-fx
  :set-alert
  (fn [cfx [_ msg]]
    {:db (assoc (:db cfx) :alert-message msg)
     :timeout {:id :alert-message
               :event [:remove-alert]
               :time 20000}}))

(rf/reg-event-db
  :remove-alert
  (fn [db]
    (dissoc db :alert-message)))

;; ...

(rf/dispatch [:alert-message "Please have a nice day!!"])

Try it out!

More posts in this Re-frame Series