Skip to content

Latest commit

 

History

History
152 lines (113 loc) · 4.96 KB

creating-components.md

File metadata and controls

152 lines (113 loc) · 4.96 KB

Creating Components

Helix's philosophy is to give you a Clojure-friendly API to raw React. All Helix components are React components, and vice-versa; any external React library can be used with Helix with as minimal interop ceremony as possible.

The easiest way to create a component using Helix is with the defnc macro. This macro creates a new function component, handling conversion of props to a CLJS data structure.

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


(defnc my-component [{:keys [name]}]
  (str "Hello, " name))

Props

React components always expect a single argument: a JavaScript object that holds all of the props passed to it.

If we were to call our component like a normal function (which you should not do! See Creating Elements), we would need to pass in a JS object to it:

(my-component (js-obj "name" "Tatiana"))
;; => "Hello, Tatiana"

The defnc macro takes care of efficiently converting this JS object to a type that works with destructuring and core functions like assoc/etc.

Interop

One thing to note is that this conversion of JS objects to CLJS data types is shallow; this means if you pass in data like a JS object, array, etc. to a prop, it will be left alone.

(defnc my-component [{:keys [person]}]
  ;; using JS interop to access the "lastName" property
  (let [last-name (.-lastName ^js person)
        first-name (.-firstName ^js person)]
    (str "Hello, " first-name " " (nth last-name 0) ".")))

;; Example calling it like a function - do not do this in your app!
;; See "Creating Elements" docs.

(my-component #js {:person #js {:firstName "Miguel" :lastName "Ribeiro"}})
;; => "Hello, Miguel R."

This is an intentional design decision. It is a tradeoff - on the one hand, it is more efficient to opt not to deeply convert JS data structures to CLJS data, and it means that you do not need to learn some Helix-specific rules when interoping with external React libraries that use higher-order components or render props.

On the other hand, it does mean there are more cases where we need to use interop syntax instead of our favorite clojure.core functions and data structures.

I do not think this is particularly bad, as you will need to understand how to interop with the library either way, and between using explicit interop syntax vs. some Helix-specific way of converting to and from data passed via React libs, I think that being explicit is better.

Higher Order Components

Higher-Order Components (HOC) are functions that take a component and return a new component, wrapped with some new functionality. The most common one is React.memo, but they are also sometimes used in libraries or apps to provide an easy way to add new behavior to arbitrary components.

Helix's defnc macro has a special metadata key you can pass to it, :wrap, which takes a collection of calls to higher-order components and will ensure that the component is wrapped in them.

(defnc memoized
  {:wrap [(helix.core/memo)]}
  [props]
  "I am memoized!")

The Memoized component will be passed to (helix.core/memo) (and any other HOCs given to the vector) using the thread-first macro.

;; This is similar to the code generated by the `defnc` macro
;; when `:wrap` is used

(defn memoized--Render [props]
  "I am memoized")

(def memoized (-> Memoized--Render
                  (helix.core/memo)))

Anonymous Components

Like functions, sometimes we want to create anonymous inline components. For that we can use the fnc macro.

(let [my-button (fnc [{:keys [class on-click] :as props}]
                  (d/button {:class class :on-click on-click}))]
  ($ my-button {:class ["foo" "bar"] :on-click #(js/alert "hi")})

Class Components

Note: Consider class components as a "last resort" action for cases where a function component cannot be used.

The defcomponent macro accepts a symbol together with a list of methods or properties. Methods are written with the syntax (name [this args] body). Properties are written with the syntax (name form). To mark a method as static, use the ^:static metadata.

Error Boundary Example

As of React 18, there is still no way to write an error boundary as a function component. Here is how we could implement an error boundary with Helix.

(defcomponent ErrorBoundary
  ;; To avoid externs inference warnings, we annotate `this` with ^js whenever
  ;; accessing a method or field of the object.
  (constructor [^js this]
    (set! (.-state this) #js {:hasError false}))

  (componentDidCatch [^js this error _info]
    (.setState this #js {:data error}))

  ^:static (getDerivedStateFromError [_this _error]
              #js {:hasError true})

  (render [^js this props ^js state]
    (if (.-hasError state)
      (d/pre
        (d/code
          (pr-str (.-data state))))
      (:children props))))