• 5 min read

Passing around components with reagent and Semantic UI

I've been happily using Semantic UI React since I first wrote about it more than a year ago. Everything in the previous post still holds true, the only thing that has changed is the version number of the semantic-ui-react package.

The CLJSJS community has been great at keeping things up to date, and I can't recall any breaking changes in the components that I've been using.

One thing I unknowingly skipped in the previous article was passing around React components as arguments for other components. I don't think I realized at the time it was possible. I was learning the minimum viable React through re-frame, which got me very far (and continues to do).

This oversight was noticed by others though. A few people reached out for advice in private, and on StackOverflow. Where I fell short other community members helped out in the Clojurians Slack.

The Problem

Looking at the tabs example in the Semantic UI React docs, you encounter this:

import React from 'react'
import { Tab } from 'semantic-ui-react'

const panes = [
  { menuItem: 'Tab 1', render: () => <Tab.Pane>Tab 1 Content</Tab.Pane> },
  { menuItem: 'Tab 2', render: () => <Tab.Pane>Tab 2 Content</Tab.Pane> },
  { menuItem: 'Tab 3', render: () => <Tab.Pane>Tab 3 Content</Tab.Pane> },
]

const TabExampleBasic = () => (
  <Tab panes={panes} />
)

export default TabExampleBasic

The same thing occurs in several places, including popups.

So the question really is how do we pass along our reagent component along as a React component?

The Widget

Sticking with my tradition of building over engineered GitHub widgets, here are some tabs in action:

The source code is available on GitHub at kennethkalmer/re-frame-semantic-ui-react-github-tabs.

Here is a truncated version of the tabs in the above video:

(ns github-repo-widget.views
  (:require [reagent.core :as reagent]
            [re-frame.core :as re-frame]
            [github-repo-widget.events :as events]
            [github-repo-widget.subs :as subs]
            [github-repo-widget.ui :as ui]))

(defn- readme-tab []
  (let [loading? @(re-frame/subscribe [::subs/repo-readme-loading?])
        readme   @(re-frame/subscribe [::subs/repo-readme])
        pane     (ui/component "Tab" "Pane")]

    [:> pane {:loading loading?}
     [:div {:dangerouslySetInnerHTML {:__html readme}}]]))


(defn- stats-tab []
  (let [loading? @(re-frame/subscribe [::subs/repo-info-loading?])
        pane     (ui/component "Tab" "Pane")]

    [:> pane {:loading loading?}
     ;; ...
     ]))


(defn- repo-tabs []
  (let [panes [{:menuItem "Readme"
                :render #(reagent/as-component [readme-tab])}
               {:menuItem "Stats"
                :render #(reagent/as-component [stats-tab])}]
        tab (ui/component "Tab")]

    [:> tab {:panes panes}]))

The solution

Reagent gives us reagent.core/as-component, which is exactly the interop we need to turn our Reagent component into a React component for these cases.

In the case of the tab panes, Semantic UI React expects a function that returns a component as a value for the render property. In other places it expects the component directly, as seen with popups:

(defn info-icon
  ([message]
   (info-icon {} message))

  ([options message]
   (let [popup (component "Popup")
         icon  (component "Icon")]

     [:> popup
      {:trigger (reagent/as-component [:> icon
                                          (merge {:name "info"}
                                                 options)])}
      " "
      message])))

Here the trigger property expects another component, not a function that returns the component.

In close

Being able to use Semantic UI React directly in ClojureScript without going through some insane incantations or rituals is a testament to the amazing work done by the Reagent contributors.

It is also a testament to Clojures pragmatic approach of embracing the language/environment that hosts it.

comments powered by Disqus