Events¶
Every state change in a re-frame2 app starts with an event. This page covers what an event looks like, how you dispatch one, and what its handler may do.
Event shape¶
An event is a vector. The first element is the id, usually a namespaced keyword. Further elements are optional facts: a single value, or a payload map when there are several:
Ids are the vocabulary of the application. Name what the user intended (:todo/add)
or what happened (:todo/added), rather than how a view is built
(:add-button-clicked).
Timers, HTTP replies, route loaders, and button clicks all use this same shape, so tools can show everything that happened to the app as one list of events.
Keep the payload plain data. A function, promise, DOM node or js/RegExp in an event
draws a :rf.warning/non-serialisable-event-payload development warning, because
replay and the event history treat an event as a value. Pass an id or a plain value
instead. An instant is plain data: a js/Date, which is what #inst reads as, draws
no warning.
Dispatch¶
You send an event with dispatch. Inside a reg-view, dispatch (and
subscribe) are provided for you:
Outside a view, for example at the REPL, call rf/dispatch and name the frame:
Dispatch does not run the handler. It enqueues the event on the frame's FIFO queue and returns immediately. The runtime dequeues it shortly after and runs the event pipeline for it. Because only the runtime runs handlers, UI callbacks stay thin and state is written one event at a time.
If the pipeline must finish before your next line of code runs, use dispatch-sync.
It belongs at boot, in tests, and at the REPL; called from inside a running
handler, it drops the event and a dev build reports
:rf.error/dispatch-sync-in-handler. Use ordinary dispatch in
views. Queue draining is covered in
Effects.
No ambient frame
dispatch must know which frame owns the queue. Inside a
view under frame-root / frame-provider, that is automatic: the dispatch a
reg-view provides has already captured its frame, so it still works when
called later from a timeout. A bare rf/dispatch from a setTimeout, a
promise or another callback with no frame in scope raises
:rf.error/no-frame-context.
;; Inside a reg-view: the provided dispatch carries the frame
[:button {:on-click #(js/setTimeout (fn [] (dispatch [:inc])) 1000)} "+ later"]
;; Outside any view: name the frame
(js/setTimeout #(rf/dispatch [:inc] {:frame :app}) 1000)
Frames covers carrying a frame with rf/capture-frame.
Handlers return descriptions¶
Register a handler with reg-event. The handler receives the world map (at
minimum {:db current-app-db}) and the event vector, and returns an effect map:
The second argument is the whole event vector, so a handler that needs the payload destructures it:
(rf/reg-event :inc-by
(fn [{:keys [db]} [_ {:keys [n]}]]
{:db (update db :value + n)}))
;; dispatched as [:inc-by {:n 5}]
The handler rules:
- Pure. Same inputs, same returned map. No
js/fetch, noswap!, no reading the clock. Impurity is described and performed later (Effects; recorded inputs are Coeffects). :dbis the next app-db value, not a patch instruction. Useassoc,update,update-in— functions that return a new map.- The runtime commits. Your function returns the next value; the pipeline writes it.
A handler returns :db, :fx, both or neither; with no :db, state is left alone.
Returning nil does nothing, so a body wrapped in when is fine. The state rules
live on app-db; effects other than :db are covered in
Effects.
Metadata when you need it¶
reg-event accepts an optional metadata map between the id and the function —
a :doc string, a payload :schema, required coeffects, interceptors. Until you
need that, the two-argument form is enough. Coeffects shows the first
metadata you are likely to use.
Without a :doc, the development build emits :rf.warning/missing-doc once per
handler, because tools show it. The examples in this guide omit :doc to stay short.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
Button "does nothing"; :rf.error/no-such-handler names the id |
You dispatched an id nobody registered; the runtime skips the event | Register the handler or fix the typo |
A timer or fetch callback throws :rf.error/no-frame-context |
Bare rf/dispatch outside a frame |
Capture the frame (Frames) |
Click does nothing, app-db is unchanged, and :rf.error/handler-exception is reported |
The handler threw; nothing was committed | Fix the handler (Errors) |
Handler gets no payload, and :rf.warning/unknown-dispatch-opt is reported |
The payload went in the options map: (rf/dispatch [:inc-by] {:n 5}) |
Put it in the event: (rf/dispatch [:inc-by {:n 5}]). The second argument takes options such as :frame |
:rf.error/effect-map-shape is reported and nothing is committed |
The top level of the effect map is closed, and the handler returned app-db itself (or another map with keys the runtime does not know) instead of {:db …} |
Wrap the next state: {:db (assoc db …)} |
:rf.error/effect-handler-bad-return is reported |
The handler returned something other than a map or nil |
Return an effect map, or nil for no change |
:rf.warning/unknown-registration-key at registration; the key is ignored |
A misspelt plain metadata key, such as :interceptor for :interceptors |
Fix the key |
| A declared coeffect never arrives and nothing warns | A misspelt namespaced key, such as :rf.cofx/require for :rf.cofx/requires; namespaced keys are not checked |
Fix the key |
| Handler can't be unit-tested | You called js/fetch / read the clock inside the body |
Return the request as an effect (Effects); declare the clock as a coeffect (Coeffects) |
An unregistered id is reported rather than thrown so that one missing handler, for example after a failed feature load, does not crash the whole app.
What events are not¶
| Not this | Why |
|---|---|
| A place to put view logic | Views stay pure; they dispatch and subscribe |
| A message bus between components | Components don't address each other; they change app-db through events |
Something you await |
Async replies arrive as later events (Effects, Async) |