react-bootstrapclojurescriptreagentre-frameshadow-cljs

React.createElement: type is invalid using react-bootstrap in ClojureScript


I'm trying to use react-bootstrap within re-frame project. I've installed react-bootstrap with

npm install react-bootstrap

and using its components like the following:

  (:require
   ;; ...
   ["react-bootstrap/Button" :as Button]
   ;; ...

(defn main-panel []
  [:div
   [:> Button "Hit me"]
  ]])

Everything works fine until I try to make a dropdown, to be more accurate, until I try to use DropdownMenu. The moment I insert it into the hiccup following the example like that

[:> Dropdown
  [:> DropdownToggle "button"]
  [:> DropdownMenu {:variant :dark}
    [:> DropdownItem "action1"]
    [:> DropdownItem "action2"]
    [:> DropdownItem "action3"]]]

I'm getting the following in the browser console:

Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object.

I'm awfully new to the whole frontend world, so I'm not sure if I'm doing something wrong or there is some bug in react-bootstrap, or reagent, or any other part of the project. Here's the MWE of this problem: https://github.com/lockie/react-bootstrap-cljs-demo


Solution

  • The documentation has a section on how to translate ES import for npm packages.

    There you'll find that ["react-bootstrap/Button" :as Button] should be ["react-bootstrap/Button$default" :as Button] instead (translating import Button from "react-bootstrap/Button";)

    This however only applies when using actual ESM code. The react-bootstrap package however is a hybrid package containing both ESM and CommonJS. shadow-cljs will not use ESM by default in that case.

    So one option is to make shadow-cljs use the ESM code instead which is done by including this in your build config.

    :js-options {:entry-keys ["module" "browser" "main"]}
    

    This will require using $default for every require since it they are all default exports.

    Another option is just using the default CommonJS code. However this appears to be packaged inconsistently, meaning that most just work but there is one exception. It works fine if I change your example code to just

    (ns react-bootstrap-cljs-demo.views
      (:require
        [re-frame.core :as re-frame]
        [react-bootstrap-cljs-demo.subs :as subs]
        ["react-bootstrap/Button" :as Button]
        ["react-bootstrap/Dropdown" :as Dropdown]
        ["react-bootstrap/DropdownItem" :as DropdownItem]
        ["react-bootstrap/DropdownMenu$default" :as DropdownMenu]
        ["react-bootstrap/DropdownToggle" :as DropdownToggle]
        ))
    

    So, only the DropdownMenu is included in a way that requires the $default. Seems to maybe an oversight or bug on the react-bootstrap side. shadow-cljs is just following what is on disk.

    Which method you choose is up to you. Using ESM might lead to issues with other packages, or may work perfectly fine. All depends on what the packages you use actually published.

    How To Figure This Out?

    As a little mini guide: I figured this out by just logging the required :as alias an looking at it in the browser console.

    So just

    (ns react-bootstrap-cljs-demo.views
      (:require
        [re-frame.core :as re-frame]
        [react-bootstrap-cljs-demo.subs :as subs]
        ["react-bootstrap/Button" :as Button]
        ["react-bootstrap/Dropdown" :as Dropdown]
        ["react-bootstrap/DropdownItem" :as DropdownItem]
        ["react-bootstrap/DropdownMenu$default" :as DropdownMenu]
        ["react-bootstrap/DropdownToggle" :as DropdownToggle]
        ))
    
    (js/console.log "Button" Button)
    (js/console.log "Dropdown" Dropdown)
    (js/console.log "DropdownItem" DropdownItem)
    (js/console.log "DropdownMenu" DropdownMenu)
    (js/console.log "DropdownToggle" DropdownToggle)
    

    If you remove the $default from DropdownMenu you'll notice that the log line looks different than all the others and that it is an object with a default property. So you add the $default to use that property and get what you actually need.