Skip to content

Use a React component

A date picker, a chart or a rich-text editor from npm is a React component written in JavaScript. This recipe renders one inside a reg-view, feeds it a value from a subscription, and turns its callbacks into events. It uses the Reagent adapter. A UIx component passes the component to $ like any other. A Fresco view declares it with h/defhost instead (Interop).

For JavaScript developers

[:> DatePicker {:selected date :on-change f}] is <DatePicker selected={date} onChange={f} />. The component itself is unchanged; only the call site is hiccup.

1. Render it with [:>]

Install the package with npm and require it. A vector headed by :> renders it: the component comes second, then its props, then any children.

(ns my.app.views
  (:require ["react-datepicker" :default DatePicker]
            [re-frame.core :as rf]))

(rf/reg-event :todo/set-due
  (fn [{:keys [db]} [_ id date]]
    {:db (assoc-in db [:todos id :due] date)}))

(rf/reg-sub :todo/due-date
  (fn [db [_ id]]
    (get-in db [:todos id :due])))

(rf/reg-view due-field [{:keys [id]}]
  [:> DatePicker {:selected         @(subscribe [:todo/due-date id])
                  :on-change        #(dispatch [:todo/set-due id %])
                  :placeholder-text "No due date"}])

The value comes from a subscription and a change is an event, as in any other view, so the due date lives in app-db and each change shows up in the trace.

The date picker calls onChange when the user picks a date, after due-field has rendered. The callback therefore uses the injected dispatch, which captured the view's frame during render. A bare rf/dispatch there raises :rf.error/no-frame-context (Views explains why).

A callback receives JavaScript values. This one receives a js/Date, which is the value #inst reads as in ClojureScript, so it can go into app-db as it is. Convert other JavaScript objects to data before you dispatch them.

2. Know how props cross

Reagent turns the props map into a JavaScript object before the component sees it:

You write The component receives
:on-change, :placeholder-text onChange, placeholderText
:class, :for className, htmlFor
:data-testid, :aria-label data-testid, aria-label, unchanged
a string, number, boolean, nil or function the same value
a nested map, such as {:max-width 200} a JavaScript object with camelCase keys, {maxWidth: 200}
a vector, list or set a JavaScript array made by clj->js, whose map keys are not camelCased
a keyword, such as :small its name, "small"

Children after the props map are hiccup, converted like any other child.

3. Pass hiccup where the component wants an element

Hiccup inside a prop is a vector, so it arrives as a JavaScript array rather than a React element. A prop that takes an element needs reagent.core/as-element:

(ns my.app.views
  (:require ["react-datepicker" :default DatePicker]
            [re-frame.core :as rf]
            [reagent.core :as r]))

(rf/reg-view due-field [{:keys [id]}]
  [:> DatePicker {:selected     @(subscribe [:todo/due-date id])
                  :on-change    #(dispatch [:todo/set-due id %])
                  :custom-input (r/as-element [:button.due "Set a due date"])}])

A render prop, a function the component calls for an element, follows the same rule: (fn [item] (r/as-element [:li (.-label item)])).

4. Return several elements with [:<>]

A fragment groups siblings without a wrapper element. Here a when needs to add two elements to the list:

(rf/reg-view todo-detail [{:keys [title due]}]
  [:dl
   [:dt "Title"] [:dd title]
   (when due
     [:<> [:dt "Due"] [:dd (.toLocaleDateString due)]])])

A view can return a fragment as its root in the same way. In a for, give each fragment a key, ^{:key id} [:<> …], as you would any other element.

Troubleshooting

Symptom Cause Fix
React reports "Element type is invalid" The require resolved to nil, usually a default export required with :refer, or a named export required with :default Match the library's export: ["lib" :default X] for a default export, ["lib" :refer [X]] for a named one
A callback raises :rf.error/no-frame-context It calls rf/dispatch, which looks for a frame when the component calls back Use the view's injected dispatch
Hiccup in a prop shows up as text, or the component throws when it uses the prop The prop was converted to a JavaScript array Wrap it in r/as-element
The library ignores an option inside a vector Map keys inside a vector are not camelCased Build that value with #js and the library's own key names
React warns that each child in a list needs a key A for produced [:>] or [:<>] elements without keys Add ^{:key id} to each