Components

UIx components are defined using the defui macro, which returns React elements created using the $ macro. The signature of $ macro is similar to React.createElement, with an additional shorthand syntax in the tag name to declare CSS id and class names (similar to Hiccup):

// React without JSX
React.createElement("div", { onClick: f }, child1, child2);

Inline components

Sometimes you might want to create an inline component using anonymous function. Let's take a look at the following example:

(defui ui-list [{{:keys [key-fn data item]}}]
  ($ :div
    (for [x data]
      ($ item {:data x :key (key-fn x)}))))

(defui list-item [{:keys [data]}]
  ($ :div (:id data)))

($ ul-list
  {:key-fn :id
   :data [{:id 1} {:id 2} {:id 3}]
   :item list-item})

In the example above ul-list takes item props which has to be a defui component, which means you have to declare list-item elsewhere.

With uix.core/fn it becomes less annoying:

(defui ui-list [{{:keys [key-fn data item]}}]
  ($ :div
    (for [x data]
      ($ item {:data x :key (key-fn x)}))))

($ ul-list
  {:key-fn :id
   :data [{:id 1} {:id 2} {:id 3}]
   :item (uix/fn [{:keys [data]}]
           ($ :div (:id data)))})

Component props

defui components are similar to React’s JSX components. They take props and children and provide them within a component as a single map of props.

Let's take a look at the following example:

function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

<Button onClick={console.log}>Press me</Button>;

The Button component takes JSX attributes and the "Press me" string as a child element. The signature of the component declares a single parameter which is assigned to an object of passed in attributes + child elements stored under the children key.

Similarly in UIx, components take a map of props and an arbitrary number of child element. The signature of defui declares a single parameter which is assigned a hash map of passed in properties + child elements stored under the :children key.

(defui button [{:keys [on-click children]}]
  ($ :button {:on-click on-click}
    children))

($ button {:on-click js/console.log} "Press me")

DOM attributes

DOM attributes are written as keywords in kebab-case. Values that are normally strings without whitespace can be written as keywords as well, which may improve autocompletion in your IDE.

($ :button {:title "play button"
            :data-test-id :play-button})

children

Similar to React, child components are passed as children in the props map. children is a JS Array of React elements.

(defui popover [{:keys [children]}]
  ($ :div.popover children))

:ref attribute

Refs provide a way to refer to DOM nodes. In UIx ref is passed as a normal attribute onto DOM elements, similar to React. use-ref returns a ref with an Atom-like API: the ref can be dereferenced using @ and updated with either clojure.core/reset! or clojure.core/swap!.

(defui form []
  (let [ref (uix.core/use-ref)]
    ($ :form
      ($ :input {:ref ref})
      ($ :button {:on-click #(.focus @ref)}
        "press to focus on input"))))

UIx components don't take refs because they are built on top of React's function-based components which don't have instances.

When you need to pass a ref into child component, pass it as a normal prop.

(defui text-input [{:keys [ref]}]
  ($ :input {:ref ref}))

(defui form []
  (let [ref (uix.core/use-ref)]
    ($ :form
      ($ text-input {:ref ref})
      ($ :button {:on-click #(.focus @ref)}
        "press to focus on input"))))

Class-based components

Sometimes you want to create a class-based React component, for example an error boundary. For that there's the uix.core/create-class function.