The Problem
Some validations can't run on the client. "Is this email already taken?" requires a server round-trip. You need to debounce the request so you're not hitting the server on every keystroke, track whether a request is still pending, inject server errors into the form state, and prevent submission while the check is in flight. Doing all of this manually for every field that needs server validation is tedious and error-prone.
Fork's send-server-request handler wraps this
entire flow. You configure it per-field, and Fork manages the timing,
waiting state, error injection, and submission blocking.
Basic Server Request
Here's a complete example: an email field that checks server-side availability on every change, debounced to 500ms:
(ns app.core
(:require
[fork.re-frame :as fork]
[re-frame.core :as rf]))
(rf/reg-event-fx
:check-email
(fn [_ [_ {:keys [values errors path state]}]]
;; only check if client-side validation passes
(when (empty? (get errors "email"))
;; faking a server request
{:dispatch-later
[{:ms 1000
:dispatch [:email-response path
(get values "email")]}]})))
(rf/reg-event-fx
:email-response
(fn [{db :db} [_ path email]]
{:db
(if (= "taken@example.com" email)
(-> db
(fork/set-error path "email" "Email already taken")
(fork/set-waiting path "email" false))
(fork/set-waiting path "email" false))}))
(defn email-form []
[fork/form {:path :form
:prevent-default? true
:validation
#(cond-> {}
(empty? (get % "email"))
(assoc "email" "Email is required"))
:on-submit #(js/alert (:values %))}
(fn [{:keys [form-id values errors server-errors
handle-change handle-blur handle-submit
send-server-request]}]
[:form
{:id form-id
:on-submit handle-submit}
[:input
{:name "email"
:value (values "email")
:on-blur handle-blur
:on-change
(fn [evt]
(handle-change evt)
(send-server-request
{:name "email"
:value (fork/retrieve-event-value evt)
:set-waiting? true
:clean-on-refetch ["email"]
:debounce 500}
#(rf/dispatch [:check-email %])))}]
[:div (or (get errors "email")
(get server-errors "email"))]
[:button {:type "submit"} "Submit"]])])Config Options
The first argument to send-server-request is
a config map:
| Prop | Type | Default | Description |
|---|---|---|---|
:name | string | — | Required. The field name. Used as a key for internal state tracking (debounce timers, waiting flags). |
:value | any | — | The current field value. Use fork/retrieve-event-value to extract it from the DOM event. If omitted, the callback receives the previous value. |
:evt | keyword | — | Set to :on-blur to mark the field as touched when the server request fires. Relevant for blur-triggered validations. |
:set-waiting? | boolean | — | Defaults to true. When true, sets a waiting flag that prevents form submission until the server responds. |
:clean-on-refetch | vector | — | Field names whose server-side state (errors, waiting) should be cleared before each new request. Prevents stale errors from lingering. |
:debounce | number (ms) | — | Debounce delay in milliseconds. The callback fires only after the user stops triggering events for this duration. Mutually exclusive with :throttle. |
:throttle | number (ms) | — | Throttle interval in milliseconds. The callback fires at most once per interval. Mutually exclusive with :debounce. |
The second argument is a callback function that performs the actual
server request. It receives a map with :state,
:path, :values,
:touched, :errors,
and :dirty.
Resolving the Waiting State
When :set-waiting? is true, Fork blocks
submission until you explicitly clear the waiting flag. After your
server logic resolves, call set-waiting
with false:
;; In your response handler:
(rf/reg-event-fx
:email-response
(fn [{db :db} [_ path result]]
{:db
(if (:error result)
;; Set error AND clear waiting
(-> db
(fork/set-error path "email" (:message result))
(fork/set-waiting path "email" false))
;; Just clear waiting (no error)
(fork/set-waiting path "email" false))}))Submission with Server Messages
For post-submission server responses (success or failure messages),
use set-server-message:
(rf/reg-event-fx
:submit-success
(fn [{db :db} [_ path result]]
{:db (-> db
(fork/set-submitting path false)
(fork/set-server-message path "Saved!"))}))
(rf/reg-event-fx
:submit-failure
(fn [{db :db} [_ path]]
{:db (-> db
(fork/set-submitting path false)
(fork/set-server-message path "Something went wrong."))}))
;; In the form component:
(fn [{:keys [on-submit-server-message ...]}]
[:div
;; ... form fields ...
(when on-submit-server-message
[:p.message on-submit-server-message])])Next Steps
- Validation — client-side validation with any library
- API Reference — every handler and option