Skip Navigation

The distinction between Reagent component and React component

Published: 2021-03-21

#clojure

At work, I rarely have to think about the distinction between a Reagent component and a React component since Reagent does such a good job providing a coherent and simple API to use. (Plus, we don’t use any extern JS component libraries.)

I came across the StackOverflow question the other day. While researching for the answer, it occurred to me that the simplicity of Reagent API can sometimes trick people up. The focus of this article isn’t so much about the original SO question, though I will use the example in the post, but more about sharing some easy-to-overlook information for the Reagent users.

The distinction

React components are classes that extends the React.Component class. Example:

// JSX style
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// JS style
class Hello extends React.Component {
  render() {
    return React.createElement('div', null, `Hello ${this.props.toWhat}`);
  }
}

As oppose to React components, Reagent components in the most simplified form (also called as Form-1 component) are just Clojure functions that returns Clojure data in Hiccup style.

(defn welcome [props]
  [:h1 {} (:name props)])

There are other ways to define a Reagent component as described in the official documentation on Creating Reagent Components.

Although this may seem counter-intuitive, since I’m already calling it “component”, the Reagent component above is just a pure Clojure function and there is no more hidden magic behind it. You can “invoke” it like any other functions in Clojure, and it’ll return a Hiccup markup without any surprise:

(welcome {:name "Steve"})
;; => [:h1 {} "Steve"]

The returned markup data is plain Clojure data, no rendering, no reactivity, no magic, just pure Clojure data, persistent and immutable. Please be aware, in a Reagent app, seldom should you “invoke” the Reagent component like this. Instead, you should “render” it by explicitly invoking the reagent.dom/render function to render the top-level Reagent component.

Rendering

What the reagent.dom/render function does is:

  1. Compile the top level Reagent component (and all the nested children components) into React components,
  2. Invoke JS function ReactDOM.render(...) with the compiled React components to render the DOM elements.

Or in the form of a flow chart:

Reagent       Compile   React        Render
Components    --------> Components   ------->  DOM
(Clojure Fns)           (JS Classes)

Most of the time, you don’t have to think about this because all components are Reagent components. Thus it’s easy to overlook the Reagent’s part in compiling the Clojure functions into React Components. (This might be a reason why some other libraries prefer explicit macros to define a Component like rum’s defc or fulcro’s defsc - to make the compilation explicit to the user.) However, when it comes to interop with React component libraries, we need to start thinking more clearly about which components are React and which are Reagent.

Interop with React

The official website has an excellent article about Interop with React. Check it out if you aren’t so familiar with it. For my brief React interop history, I was almost exclusively using the :> tag to render React component from the Reagent. However, things could get more tricky like the Example: “Decorator” Higher-Order Components

Gotcha

Contrary to my instinct, r/adapt-react-class isn’t the reverse of r/reactify-component. That is, a React component won’t make it a round trip back to itself after threading through r/adapt-react-class and r/reactify-component. The r/adapt-react-class takes a React component and returns a reagent.impl.template.NativeWrapper, whereas r/reactify-component takes a Clojure callable and coerce it into a React component. So be aware.

Example: Stylized Material UI Component

Below is the example I worked out for the StackOverflow question. Pay attention to the the calls to r/adapt-react-class and r/reactify-component. Since the MuiValueLabel component is a React component from the external library, we need to use r/adapt-react-class so we can use it with the other Reagent library. The props value for :ValueLabelComponent needs to be a React component because slider is an adapted react class (just as the mui-value-label), hence the function call to r/reactify-component.

(ns example.core
  (:require
   [reagent-material-ui.core.slider :refer [slider]]
   [reagent-material-ui.styles :as styles]
   [reagent.core :as r]
   [reagent.dom :as rdom]
   [reagent.impl.util :as rutil]
   ["@material-ui/core/Slider/ValueLabel" :as MuiValueLabel]))

(def mui-value-label
  "The NativeWrapper of Material UI's ValueLabel class."
  (r/adapt-react-class (.-default MuiValueLabel)))

(def with-offsets
  "The higher-order component that provides 50px down and right offsets."
  (styles/with-styles {:offset {:top 50 :left 50}}))

(defn value-label-with-offsets
  "A Reagent component that renders the MUI ValueLabel with offsets of 50px down
  and right."
  [props]
  [(with-offsets mui-value-label) props])

(defn main
  "The top level Reagent component."
  []
  [slider
   {:defaultValue        [31 37]
    :valueLabelDisplay   "on"
    :ValueLabelComponent (r/reactify-component
                           ;; Nay
                           #_(with-offsets mui-value-label)
                           ;; Ok
                           #_(fn [props] [(with-offsets mui-value-label) props])
                           ;; Yay
                           value-label-with-offsets)}])

(defn ^{:after-load true, :dev/after-load true}
  mount []
  (rdom/render [main] (js/document.getElementById "app")))

(defn ^:export init []
  (mount))

This is mentioned in the Gotcha. The real tricky part is that reagent-material-ui.styles/with-styles returns a reagent.impl.template.NativeWrapper and r/reactify-component cannot handle it like it handles Reagent components. (See the snippet below.) Therefore, I needed to wrapped it inside another function call to make it work.

(r/reactify-component
  ;; A reagent.impl.template.NativeWrapper
  (r/adapt-react-class
    (.-default MuiValueLabel)))
;; => nil

(r/reactify-component
  ;; A inline Reagent component
  (fn [props]
    [(r/adapt-react-class
       (.-default MuiValueLabel))
     props]))
;; => reagent<N>

In this case, it was extremely helpful to have a running REPL to help me figuring out the types of the components.

Epilogue

Spending time reading the Reagent source code was a lot of fun for me. Here I just want to share a few interesting functions I saw:

reagent.impl.template NS: - vec-to-elem: convert a vec into a JS React element via make-element - make-element: make a JS React element - as-element - valid-tag?: return true if a tag is a symbol, keyword, string, function, or NativeWrapper.

reagent.impl.component NS: - create-class: Creates JS class (that extends React.Component) based on provided Clojure map.

reagent.impl.util NS: - dash-to-prop-name: turn kebab-case prop names into camelCase.

This work is licensed under a Creative Commons Attribution 4.0 International License.