Skip to content

Latest commit

 

History

History
202 lines (147 loc) · 6.19 KB

creating-elements.md

File metadata and controls

202 lines (147 loc) · 6.19 KB

Creating Elements

React Elements are a way to represent instances of components as data. Components are expected to return React Elements objects. Helix provides a number of helpful tools to aid in Element creation.

$ macro

The $ macro takes a component type (string, keyword, or symbol referring to a Component), optionally some props, and any children, and returns a React Element with that same information, like React.createElement.

($ "div" "hello")
;; => #js {:type "div" :props #js {:children "hello"}}

($ my-component {:data {:foo "bar"}} "red text in a div")
;; => #js {:type my-component :props #js {:data {:foo "bar"}}}

($ my-component
   ($ "div" "first")
   ($ "div"
      "second"
      ($ "span" "last")))
;; => #js {:type my-component
;;         :props #js {:children #js [#js {:type "div"
;;                                         :props #js {:children "first"}}
;;                                    #js {:type "div"
;;                                         :props #js {:children #js ["second"
;;                                                                    #js {:type "span"
;;                                                                         :props #js {:children "last"}}]}}]}}

When a map literal is passed to the second argument, it will treat this map as props to provide the component and will be compiled and output as a JS object. This is to avoid converting the map to a JS object at runtime, as well as seamlessly using helix and 3rd party React components together.

This conversion is shallow. If you pass in any CLJS type into a prop, it will be passed in as-is. All keys are converted to strings without any munging.

A few exceptions are documented below.

Native elements and props

For "native" elements (in React DOM this is any string like "div", "span", etc.), all prop keys will be converted from kebab-case to camelCase and several props will be specially transformed.

An element is determined to be "native" and be subject to these transformations if:

  • It is a string e.g. "div"
  • OR it is a keyword e.g. :div
  • OR it is inferred to be a string or keyword (experimental)
  • OR it has metadata key :native set to true
($ "div" {:style {:color "red"
                  :background "green"}})
;; => #js {:type "div"
;;         :props #js {:style #js {:color "red"
;;                                 :background "green"}}
;;         ...}


($ ^:native SomeComponent {:on-click do-thing})
;; => #js {:type SomeComponent :props #js {:onClick do-thing}}

Other special props:

  • The :class prop is renamed to :className
  • The :for prop is renamed to :htmlFor

Dynamic props

Props that need to be determined at runtime can be passed in and merged with the props map using the & or :& key. This is colloquially referred to as "spread props", which is a reference to usage of JS' spread operator with JSX. The syntax is meant to mirror dynamic arity in function definitions.

;; dynamic props
(def extra-props {:prop3 "baz"})

($ my-component {:prop1 "foo" :prop2 "bar" & extra-props})
;; => #js {:type my-component :props #js {:prop1 "foo" :prop2 "bar" :prop3 "baz"}}

Props in the dynamic map will override props that are defined statically.

(def extra-props {:b 3})

($ my-component {:a 1 :b 2 & extra-props})
;; => #js {:type my-component :props #js {:a 1 :b 3}}

This syntax is also useful when you want to pass a non-literal map to a component:

(let [m {:foo "bar"}]
  ;; This will not work; `m` will be treated as a child:
  ($ my-component m)
  ;; This will work; `my-component` will recieve `m` as props:
  ($ my-component {& m}))

You can use either the symbol & or the keyword :&, as some tools like Cursive, joker, etc. use static analysis to find unimported symbols, which & looks like. Try and be consistent with which you use, especiallly on a team!

helix.dom

The helix.dom namespace contains helper macros for creating React DOM Elements.

All macros use the same props syntax and rules as $ for native elements.

(ns my-app.feature
  (:require [helix.dom :as d]))


(d/div "hello")

(d/div {:style {:color "red"}} "red text")

;; spread props
(d/input {:type "text" & other-props})

Other helpful tools

Fragments

helix.core/<> is a helper macro to create React Fragments

(<> ($ "div") ($ "span"))
;; => #js {:type react/Fragment :props #js {:children #js [ ... ]}}

Context providers

helix.core/provider is a helper macro to create a Provider element based on a React Context value.

(def my-context (react/createContext "default"))

(helix.core/provider
  {:context my-context
   :value "overrides default value"}
  ($ some-component)
  ($ other-component))
;; => #js {:type (.-Provider my-context)
;;         :props #js {:value "overrides default value"
;;                     :children #js [ ... ]}}

(Then, inside a child component, you would access it like this: (= "overrides default value" (hooks/use-context my-context)).)

Suspense boundaries

helix.core/suspense is a helper macro to create a React Suspense boundary.

(helix.core/suspense
  {:fallback ($ spinner)}
  ($ "div"
     ($ other-component)))
;; => #js {:type react/Suspense
;;         :props #js {:fallback #js {:type Spinner}}
;;                     :children #js [ ... ]}

Factory functions

Factory functions can be used instead of calls to $. Factory functions will parse their props at runtime from a CLJS map to a JS object, thus being slightly slower.

(ns my-app.feature
  (:require [helix.core :refer [defnc factory]]
            [helix.dom :as d]))

(defnc MyComponent [{:keys [name on-click]}]
  (d/a {:on-click #(on-click name)}
       "Greetings " name "!"))

(def my-component (factory MyComponent))


(my-component {:name "Uma" :on-click #(js/alert (str "hello," %))})
;; => {:type MyComponent
;;     :props #js {:name "Uma" :on-click #function[...]}}