Skip to content

Latest commit

 

History

History
223 lines (167 loc) · 7.47 KB

components.md

File metadata and controls

223 lines (167 loc) · 7.47 KB

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);
;; UIx
($ :div#id.class {:on-click f} child1 child2)
(ns my.app
  (:require [uix.core :refer [defui $]]))

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

(defui text-input [{:keys [value type on-change]}]
  ($ :input {:value value
             :type type
             :on-change #(on-change (.. % -target -value))}))

(defui sign-in-form [{:keys [email password]}]
  ($ :form
    ($ text-input {:value email :type :email})
    ($ text-input {:value password :type password})
    ($ button {} "Sign in")))

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")

Performance optimisation

To avoid unnecessary updates, UIx components can be memoised using uix.core/memo function or ^:memo tag.

(defui ^:memo child [props] ...)

(defui parent []
  ($ child {:x 1}))

As long as props doesn't change when parent is updated, the child component won't rerun. Read React docs on memoisation to learn when to use this optimisation.

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.

(def error-boundary
  (uix.core/create-class
    {:displayName "error-boundary"
     :getInitialState (fn [] #js {:error nil})
     :getDerivedStateFromError (fn [error] #js {:error error})
     :componentDidCatch (fn [error error-info]
                          (this-as this
                            (let [props (.. this -props -argv)]
                              (when-let [on-error (:on-error props)]
                                (on-error error)))))
     :render (fn []
               (this-as this
                 (if (.. this -state -error)
                   ($ :div "error")
                   (.. this -props -children))))}))

($ error-boundary {:on-error js/console.error}
  ($ some-ui-that-can-error))

Props transferring via spread syntax

One thing that is sometimes useful in React/JavaScript, but doesn't exist in Clojure, is object spread syntax for Clojure maps (see object spread in JS). It's often used for props transferring to underlying components and merging user-defined props with props provided by third-party React components.

function Button({ style, ...props }) {
  return (
    <div style={style}>
      <MaterialButton {...props} />
    </div>
  );
}

In Clojure you'd have to merge props manually, which is not only verbose, but also won't work with third-party React components that supply props as JS object, because in UIx props is Clojure map.

(ns app.core
  (:require [uix.core :as uix :refer [defui $]]
            ["react-hook-form" :as rhf]))

(defui form [{:keys [input-style]}]
  (let [f (rhf/useForm)]
    ($ :form {:on-submit (.-handleSubmit f)}
      ($ :input (merge {:style input-style}
                       ;; can't merge JS object returned from .register call
                       ;; with Clojure map above
                       (.register f "first-name"))))))

For this specific reason UIx adds syntactic sugar in $ macro to support props merging regardless of their type. To spread (or splice) a map or object into props, use :& key. This works only at top level of the map literal: {:width 100 :& props1}. When spreading multiple props, use vector syntax {:width 100 :& [props1 props2 props3]}.

(defui form [{:keys [input-style]}]
  (let [f (rhf/useForm)]
    ($ :form {:on-submit (.-handleSubmit f)}
      ($ :input {:style input-style :& (.register f "first-name")}))))

Note that props spreading works the same way how merge works in Clojure or Object.assign in JS, it's not a "deep merge".