Storybook.JS is a very interesting development tool from JavaScript ecosystem1. This tutorial shows how we can use it with Shadow-CLJS. The code resides at storybook.js-with-shadow-cljs repo.
Prerequisites
The tutorial uses the following:
- Java version 11
- Node.js version 14.15.4
- Reagent version 1.0.0
- Shadow-CLJS version 2.11.8
- Storybook.JS version 6.1.17
Make sure the first two are installed prior the tutorial. The others will be installed along the way.
Getting a simple React app running
Let’s create the scaffold to kick-start:
$ mkdir acme
$ cd acme
$ npm init # just keep pressing enter until the prompt ends
$ npm install --save-dev shadow-cljs
In the generated package.json, add a helper script to launch shadow-cljs
and automatically compile when it detect changes:
"scripts": {
"dev": "shadow-cljs watch frontend"
}
The script uses the :frontend profile defined in shadow-clj.edn for
ClojureScript compiler. Run npx shadow-cljs init to generate the skeleton
shadow-cljs.edn file and edit it as follows:
|
|
Line 8 adds Reagent as a dependency; lines 11 and 12 create the
profile :frontend (that matches the npm script’s shadow-cljs watch command).
This profile specifies that the build targets the browser and should
generate the file main.js (‘cos of the :main key) that will invoke
acme.core/init function at initialization. Let’s implement init that uses
a simple Reagent component in src/main/acme/core.cljs:
|
|
Simple enough: a custom header component that outputs the given text in
an h1 element and the init function that renders the header. To see this
glorious app render, create the public/index.html as follows:
|
|
By default, Shadow-CLJS generates the output to public/js,
hence the highlighted line (line 9). When the page is ready, init will run and
renders the header component. Before running npm run dev, add dev-http
to shadow-cljs.edn to configure the dev-server to listen to port 8080 and
serve artifacts from public directory:
|
|
With all these set up, run npm run dev and load
the page localhost:8080 in your favorite browser; you should see “Hello,
World!”:

Some cleanup
Before integrating with Storybook.JS, let’s do some cleaning
up: extract the custom header component to its own namespace and make
acme.core/init use that extracted one instead. First, the extracted
component at src/main/acme/components/header.cljs:
|
|
Then, in src/main/acme/core.cljs, delete header function and require
the header component namespace (as shown in line 2 below):
|
|
Adding Storybook.JS to the mix
Time to add Storybook.JS to the project. Install it with
npm install --save-dev @storybook/react; then create .storybook/main.js
with the following contents to configure Storybook.JS to look for stories
in public/js/stories directory:
|
|
Update shadow-cljs.edn to create a new profile specifically for stories
that outputs the transpiled stories to public/js/stories too:
|
|
A few notable points on the new :stories profile:
:entriesspecifies the namespaces to transpile to stories; unlike:frontendprofile that specifies the target filename to output to (main.js), Shadow-CLJS uses the namespace as the output filename, e.g.,acme.stories.header_stories.js:targetstates the build should target npm module which works for Storybook.JS2
Add two script commands to package.json to ease the auto-compilation of
stories and to start Storybook.JS:
|
|
And finally, the story. Let’ create a very simple story at
src/stories/acme/stories/header_stories.cljs that says “Hello, World!”:
|
|
The snippet above uses Component Story Format, hence the need to
add the metadata ^:export to default and HelloWorldHeader. Because
Storybook.JS operates on React components, reactify-component
at line 7 turns the Reagent component into a React
one.3 With all these preparation, run npm run dev-stories in one console,
and npm run storybook in another. You should see Storybook.JS
render our first story:

For the fun of it, let’ append another story to header-stories:
|
|

Wrapping up
That concludes this tutorial on using Storybook.JS with Shadow-CLJS. In this case, we are using Reagent to create the components for Storybook.JS to render. It shouldn’t be that difficult to adapt the setup to work with other ClojureScript rendering libraries, e.g., Helix.
-
Shadow-CLJS has a new
:esmtarget that outputs to ES Modules, but as of this writing, it is cumbersome to use (the^:exportmetadata hint isn’t working, thus requiring the need to declare all exports inshadow-cljs.edn. ↩︎ -
Refer to Reagent’s tutorial on Interop with React for more information. ↩︎