Tutorial 21 - A reasoned porting of the Official React tutorial to Reagent (Part I)

March 8, 2017 · View on GitHub

In the previous tutorial we enhanced the build.boot file to be able to install a CLJ/CLJS library to the local maven repository and finally publish it to clojars.

As it has been said in previous tutorials aimed at creating a kind of confidence with the ClojureStript programming language, by adopting the domina library for DOM manipulation we have been almost prehistoric from the point of view of building User Interfaces with CLJS.

In this tutorial of the series we're going to fill that gap by introducing Reagent, a very well known minimalist ClojureScript interface to React.js. React is a JavaScript library created by Facebook for building User Interfaces, that has recently received a lot of attention from many places.

Preamble

To start working, assuming you've git installed, do as follows:

git clone https://github.com/magomimmo/modern-cljs.git
cd modern-cljs
git checkout se-tutorial-18
git checkout -b reagent-tutorial

Introduction

Obviously, this is not a tutorial on React and I'm not going to explain its details. Still, I think that by porting to Reagent one of the React introductory tutorials step by step, we could better appreciated the Reagent minimalism and eventually understand its different approach from React itself.

Install React Tutorial

To get an idea of the final web application created step by step in the React Tutorial you need Node.js to be installed on your computer.

Install Node Version Manager (NVM)

I personally prefer to be able to install node by using nvm, a simple script to install and manage different node versions.

Open your terminal and do the following:

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash

NOTE 1: On OSX, if you get nvm: command not found after running the install script, your system may not have a .bash_profile file where the command is set up. Simply create one with touch ~/.bash_profile and run the install script again.

You can now install node by issuing the following command at the terminal:

nvm install 6

Verify that both node.js and npm have been installed:

node -v
v6.10.0
npm -v
3.10.10

Clone and run the React Tutorial

Now that you have node and npm installed on your computer, clone and run the React Tutorial final web application as follows:

git clone https://github.com/reactjs/react-tutorial.git
Cloning into 'react-tutorial'...
remote: Counting objects: 546, done.
remote: Total 546 (delta 0), reused 0 (delta 0), pack-reused 546
Receiving objects: 100% (546/546), 109.85 KiB | 0 bytes/s, done.
Resolving deltas: 100% (263/263), done.

git checkout 2be1a2d

NOTE 2: We set the head of the git repository to a specific commit in order to remain consistent with the earlier versions of this tutorial.

cd react-tutorial/
npm install
react-tutorial@0.0.0 /Users/mimmo/temp/react-tutorial
├─┬ body-parser@1.17.0 
...
  └── vary@1.1.0 
PORT=3001 node server.js
Server started: http://localhost:3001/

NOTE 3: I set node's http server port to 3001 so that its express server would not clash with the default 3000 port number of the clojure web server we're going to launch later.

Play with the React web application

Now visit the URL localhost:3001 in your browser and you should see something like this:

React Tutorial

As you see, this is a very basic web application, but it contains important concepts to be grasped for better understanding React and the way Reagent interfaces with it.

Post a couple of new comments by using its comment form. You'll note the list of comments is updated without a full page refresh.

React Tutorial new comments

Nothing new under the sun. What is new, aside from the performance that we can't appreciate with such a simple sample, is well hidden under the hood.

NOTE 4: In the above image you'll note that the first newly added comment contains a link and the second comment contains a word in bold. This is because the <input> element for the comment is able to parse markdown text.

Your first React Component

Now that you have an idea about the final behavior of the web application implemented in the React Tutorial, let's get started by following it step by step to progressively port it to Reagent.

Stop the running node server and issue the following commands at the terminal to start from scratch:

git reset --hard
HEAD is now at 2be1a2d... Use 15.0.1
git checkout -b reagent-tutorial
Switched to a new branch 'reagent-tutorial'
rm public/scripts/example.js
touch public/scripts/example.js

Open the example.js source file with your preferred editor and paste into it the following code extracted from the beginning of the official React Tutorial:

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox />,
  document.getElementById('content')
);

Rerun the tutorial web app:

PORT=3001 node server.js
Server started: http://localhost:3001/

and then reload the localhost:3001 page. You should see your very first and simple React Component saying Hello, world! I am a CommentBox.

The above lines mix into JS code a kind of HTML code, JSX code in React parlance. It first defines a new class, named CommentBox, which is a UI component containing one method only: render(). This method uses the JSX syntax to declare the structure of the component itself. In this very simple case, it is just a div component and the text node Hello, world! I am a CommentBox.

This newly defined ComponentBox component class is then instantiated by the ReactDOM.render() method, which also attaches it to the content element id of the HTML page.

Let's take a look at the index.html from the public directory of the react-tutorial project folder.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>React Tutorial</title>
    <!-- Not present in the tutorial. Just for basic styling. -->
    <link rel="stylesheet" href="css/base.css" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.6.16/browser.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/babel" src="scripts/example.js"></script>
    <script type="text/babel">
      // To get started with this tutorial running your own code, simply remove
      // the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>

There are a few things to be noted here:

  1. <div id="content"></div> represents the root HTML element to which the CommentBox component instance is to be attached;
  2. the inclusion of the marked JS library for rendering mardown text;
  3. the <script type="text/babel" src="scripts/example.js"></script> script tag loading the example.js file we just coded

The JSX code contained in the example.js file can't be interpreted as is in a browser. It first needs to pass through the babel transpiler to be transformed into JS source code, which will be compatible with almost any browser. That's why the type attribute of the latest two script tags are set to text/babel.

Prepare the field

Before starting the CLJS development environment, we first want to add the latest available Reagent library to the build.boot of the modern-cljs project.

(set-env!
 ...
 :dependencies '[
                 ...
                 [reagent "0.6.0"]
                 [cljsjs/marked "0.3.5-0"]
                 ])
...

Note that we also added the cljsjs/marked "0.3.5-0" JS external library. This is the same JS library used in the React Tutorial, and it is packaged to be used in a CLJS project. We'll see its use later.

Now create the reagent.html file in the html directory of the modern-cljs

cd /path/to/modern-cljs
touch html/reagent.html

and copy into it the following very simple html code:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Reagent Tutorial</title>
    <link rel="stylesheet" href="css/base.css" />
  </head>
  <body>
    <div id="content"></div>
    <script src="main.js"></script>
  </body>
</html>

As you see, we linked the main.js file that will be created by the CLJS compilation as soon as we issue the boot dev command to launch the CLJS development environment.

The only element of the body, aside from the cited script tag, is a div. This is very typical of any Single Page Application (SPA) and it resembles the index.html file from the React Tutorial.

Also copy the base.css file from the React Tutorial to the html/css directory of the modern-cljs project to obtain the same basic page style.

cp /path/to/react-tutorial/public/css/base.css html/css/

We are almost done. Before launching the boot dev command, create the reagent.cljs file and require both the reagent.core and the cljsjs.marked namespaces into it.

touch src/cljs/modern_cljs/reagent.cljs
(ns modern-cljs.reagent
  (:require [reagent.core :as r]
            [cljsjs.marked]))

NOTE 5: As we saw in previous tutorials, to be able to use in the bREPL a library never used before by other namespaces of the project, we first need to require its namespace in a CLJS file, otherwise the bREPL is not able to access it.

Your first Reagent Component

Launch the CLJS development environment as usual:

boot dev
Starting reload server on ws://localhost:54336
...
Writing main.cljs.edn...
Compiling ClojureScript...
 main.js
Writing target dir(s)...
Elapsed time: 32.980 sec

NOTE 6: we could have launched boot tdd instead. But at the moment we're not interested in executing any tests. We only want to learn about Reagent by interacting with it at the bREPL.

As usual, open a new terminal and launch the boot client and the bREPL on top of it. Finally, open localhost:3000/reagent.html in your browser.

boot repl -c
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.7.0
Java HotSpot(TM) 64-Bit Server VM 1.8.0_66-b17
        Exit: Control+D or (exit) or (quit)
    Commands: (user/help)
        Docs: (doc function-name-here)
              (find-doc "part-of-name-here")
Find by Name: (find-name "part-of-name-here")
      Source: (source function-name-here)
     Javadoc: (javadoc java-object-or-class-here)
    Examples from clojuredocs.org: [clojuredocs or cdoc]
              (user/clojuredocs name-here)
              (user/clojuredocs "ns-here" "name-here")
boot.user=>
boot.user=> (start-repl)
<< started Weasel server on ws://127.0.0.1:54355 >>
<< waiting for client to connect ... Connection is ws://localhost:54355
Writing boot_cljs_repl.cljs...
 connected! >>
To quit, type: :cljs/quit
nil
cljs.user=>

NOTE 7: as soon as you visit localhost:3000/reagent.html, the bREPL connects to the browser's JS engine and is ready to evaluate CLJS expressions.

Now require the reagent.core namespace at the bREPL and create your very first Reagent Component, which, under the hood, will become a React Component.

cljs.user> (require '[reagent.core :as r :refer [render]])

cljs.user> (defn comment-box []
             [:div "Hello, world! I'm a comment-box"])
#'cljs.user/comment-box

Believe it or not, such a simple function returning a vector is enough to create a Reagent Component corresponding to the CommentBox Component we previously created with the following JSX code:

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});

Don't you believe it? Evaluate the following form at the bREPL:

cljs.user> (render [comment-box] (.getElementById js/document "content"))
#object[Constructor [object Object]]

Do you see the Hello, world! I'm a comment-box text in the page? The above render call corresponds to the ReactDOM.render function call used with React.

NOTE 8: being clojurean, we used kebab-case names (i.e. comment-box) instead of CamelCase names (i.e. CommentBox).

Does the [:div "Hello, world! I'm a comment-box"] vector remind you of something? It uses the same hiccup syntax we're already accustomed with from the tutorials involving hiccups and enlive libraries as you can verify by yourself:

cljs.user> (require-macros '[hiccups.core :refer [html]])

cljs.user> (require '[hiccups.runtime])

cljs.user> (html [:div "Hello, world! I'm a comment-box"])
"<div>Hello, world! I'm a comment-box</div>"

Obviously, if you evaluate the newly defined comment-box function at the bREPL, it just returns a vector adhering to the hiccup syntax.

cljs.user> (comment-box)
[:div "Hello, world! I'm a comment-box"]

While the html macro from the hiccups library parses the hiccup vector to generate a corresponding html text, the render function from the reagent library parses the same hiccup vector to generate a corresponding React component to be mounted in a DOM node of an html page:

cljs.user> (doc html)
-------------------------
hiccups.core/html
([options & content])
Macro
  Render Clojure data structures to a string of HTML.
nil
cljs.user> (doc render)
-------------------------
reagent.core/render
([comp container] [comp container callback])
  Render a Reagent component into the DOM. The first argument may be 
either a vector (using Reagent's Hiccup syntax), or a React element. The second argument should be a DOM node.

Optionally takes a callback that is called when the component is in place.

Returns the mounted component instance.
nil

Note that the reagent.core namespace also offers the render-to-static-markup and render-to-string functions:

cljs.user> (doc r/render-to-static-markup)
-------------------------
reagent.core/render-to-static-markup
([component])
  Turns a component into an HTML string, without data-react-id attributes, etc.
nil

The render-to-static-markup function produces a HTML string similar to the one produced by the hiccups html macro

cljs.user> (r/render-to-static-markup [comment-box])
"<div>Hello, world! I&#x27;m a comment-box</div>"

while the render-to-string function converts the passed component into an HTML string instrumented as a React component:

cljs.user> (doc r/render-to-string)
-------------------------
reagent.core/render-to-string
([component])
  Turns a component into an HTML string.
nil
cljs.user> (r/render-to-string [comment-box])
"<div data-reactid=\".3\" data-react-checksum=\"-1197271774\">Hello, world! I&#x27;m a comment-box</div>"

The render-to-string function is very useful when you want to pre-render a page using node on the server-side.

Considering that we still have the domina library available in our project, we can simplify the previous render call as follows:

cljs.user> (require '[domina.core :refer [by-id]])

cljs.user> (render [comment-box] (by-id "content"))
#object[Constructor [object Object]]

Let's move on.

Composing components

The next step in the React Tutorial is to define two new skeleton components:

  • the CommentList component, to list comments;
  • the CommentForm component, to create new comments.

Here is the corresponding code you have to add to the example.js JSX file.

var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        Hello, world! I am a CommentList.
      </div>
    );
  }
});

var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

At the moment they have exactly the same structure as the previous CommentBox component that we're are going to redefine to include them inside it as follows:

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList />
        <CommentForm />
      </div>
    );
  }
});

Pretty logical. The div component now includes an h1, a CommentList and a CommentForm component.

If you now reload the localhost:3001 page you should see the following content:

Composing Component

Let's do the same thing with Reagent.

cljs.user> (defn comment-list []
             [:div "Hello, world! I'm a comment-list"])
#'cljs.user/comment-list
cljs.user> (defn comment-form []
             [:div "Hello, world! I'm a comment-form"])
#'cljs.user/comment-form

We can now redefine the comment-box component by incorporating into it an h1, a comment-list and a comment-form component as we just did with the CommentBox React component.

cljs.user> (defn comment-box []
             [:div
              [:h1 "Comments"]
              [comment-list]
              [comment-form]])
#'cljs.user/comment-box

Let's re-render the comment-box root component of our newly created component hierarchy:

cljs.user> (render [comment-box] (by-id "content"))
#object[Constructor [object Object]]

The reagent.html page is immediately updated and you should see the following content

Reagent Composing Components

NOTE 9: remember that you always have to define a component hierarchy with one root component only (e.g. the above :div).

So far, so good. We replicated in Reagent the same components composition with much less incidental complexity (i.e. plumbing code).

Before going on with the next step, let's evaluate again the (comment-box) function at the bREPL

cljs.user> (comment-box)
[:div
  [:h1 "Comments"]
  [#object[...]]   ;; comment-list function object
  [#object[...]]]  ;; comment-form function object

NOTE 10: the output has been manually simplified to make it more readable

The concept should be evident. This is just a standard application of the Clojure(Script) evaluation rules for vectors: each item in a vector is evaluated and when it is a symbol, it evaluates to the value of the symbol. It just happens that comment-list and comment-box are functions, so they evaluate to the corresponding function objects.

I strongly suggest to take your time to read one of the best pieces of documentation available on Reagent, because it explains very well why you should never use round parentheses when composing components, even if you eventually could:

cljs.user> (defn comment-box []
             [:div
              [:h1 "Comments"]
              (comment-list)   ;; don't do this
              (comment-form)]) ;; don't do this
#'cljs.user/comment-box
cljs.user> (comment-box)
[:div
  [:h1 "Comments"]
  [:div "Hello, world! I'm a comment-list"]
  [:div "Hello, world! I'm a comment-form"]]
cljs.user> (render [comment-box] (by-id "content"))
#object[Constructor [object Object]]

The two scenarios becomes clearer if you add, and I suggest you to do it, the React Developer Tools extension to your Google Chrome Browser.

By composing the components with the round parentheses, the Reagent render function produces the following component hierarchy

Round Parentheses

while, by composing them with the square brackets, the Reagent render function produces the following component hierarchy:

Square Brackets

When you compose components by using the round parentheses instead of the square brackets, the render function will not componentize them as React component and you'll loose the performance improvement implemented by the the VDOM differ algorithm.

Let's move on.

Passing data to components

Let's now create a Comment component, which will depend on the data passed in from its parent (i.e. ComponentList).

var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {this.props.children}
      </div>
    );
  }
});

Even without going into details with the JSX syntax, you can see that the Comment component will accept two arguments from its parent:

  1. the author of the comment (i.e. this.props.author), rendered as an h2 component;
  2. the comment itself (i.e. this.props.children), rendered as regular text node.

We can now redefine the CommentList component (i.e. the parent of the Comment component) as follows:

var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        <Comment author="Pete Hunt">This is one comment</Comment>
        <Comment author="Jordan Walke">This is *another* comment</Comment>
      </div>
    );
  }
});

Note that we have passed the author property as an attribute and the text as a child node of the Comment component.

If you reload the localhost:3001 page you should see the following content:

Pass data to components

Let's replicate the same thing with Reagent. We just need to define a function with two parameters, author and comment, which returns a hiccup vector:

cljs.user> (defn comment-component [author text]
             [:div
              [:h2 author]
              text])
#'cljs.user/comment-component

NOTE 11: the comment symbol is already taken by the cljs.core namespace. This is why we preferred to name the new component as comment-component.

cljs.user> (comment-component "Pete Hunt" "This is a comment")
[:div [:h2 "Pete Hunt"] "This is a comment"]

Again, the comment-component function can be used as component inside an hiccup vector. Redefine the comment-list component as follows:

cljs.user> (defn comment-list []
             [:div
              [comment-component "Paul Hunt" "This is a comment"]
              [comment-component "Jordan Walke" "This *another* component"]])
#'cljs.user/comment-list

Let's see the result by re-rendering the comment-box root component

cljs.user> (render [comment-box] (by-id "content"))
#object[Constructor [object Object]]

As soon as you evaluate the render function, the DOM of the reagent.html page gets updated and you should see the same content you previously saw in react.

Reagent props

Adding markdown

While playing with the final web application implemented in the React Official Tutorial, we previously discovered that it supports markdown markup in the text of a comment. Here is the solution adopted in the React Tutorial for parsing markdown marked text and then generating the corresponding HTML markup to be rendered by the browser.

var Comment = React.createClass({
  rawMarkup: function() {
    var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
    return { __html: rawMarkup };
  },

  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={this.rawMarkup()} />
      </div>
    );
  }
});

As you see, the React tutorial redefines the Comment component by adding to it a rawMarkup() function. This function returns a JS object with one property only: __html. Its value is set by the marked() function from the marked JS library.

The result of the rawMarkup() call is set as the value of the dangerouslySetInnerHTML attribute inside a span component.

If you now reload localhost:3001 you should see the word another displayed in italics.

Let's try to port this solution to Reagent. As you probably remember from the lessons involving hiccups and enlive, the hiccup syntax allows us to set element attributes in a map passed as second item of a hiccup vector, like so:

cljs.user> (html [:div [:label {:for "price"} "Price"]])
"<div><label for=\"price\">Price</label></div>"

Here we set "price" as the value of the :for attribute for the :label element. The same thing is true in Reagent as well. Let's see it at work.

Now require the cljsjs.marked namespace defined in the cljsjs/marked JS library that we added to the :dependencies section of the build.boot at the beginning of the tutorial.

cljs.user> (require '[cljsjs.marked])

NOTE 12: when you require an external JS library prepackaged for use by CLJS you can refer its symbols by using the js fictitious namespace.

Verify that the marked JS library works as expected

cljs.user> (js/marked "This is *another* comment.")
"<p>This is <em>another</em> comment.</p>\n"
cljs.user> (js/marked "This is <em>another</em> comment." #js {:sanitize true})
"<p>This is &lt;em&gt;another&lt;/em&gt; comment.</p>\n"

NOTE 13: the js/marked function expects a JS object as a second optional argument. #js tagged literal transforms a CLJS structure (a CLJS map in this case) into a JS corresponding structure (a JS object in this case). #js is not recursive. If you need to transform a nested CLJS data structure into a corresponding JS data structure use the clj->js function.

In the first call, we only passed to the js/marked function a string containing markdown markup. In the second call we also passed a JS object as a second argument to sanitize a string containing HTML markup.

Just for curiosity, test the js/marked function inside a call from the hiccups library using a map to set the dangerouslySetInnerHTML attribute for a span element as follows:

cljs.user> (html [:span {:dangerouslySetInnerHTML (js/marked "This is *another* comment" #js {:sanitize true})}])
"<span dangerouslySetInnerHTML=\"&lt;p&gt;This is &lt;em&gt;another&lt;/em&gt; comment&lt;/p&gt;\n\"></span>"

OK, it worked. We're now ready to redefine the comment-component component to replicate the same behavior from the React Official Tutorial.

cljs.user> (defn comment-component [author comment]
             [:div 
              [:h2 author]
              [:span {:dangerouslySetInnerHTML 
                      #js {:__html (js/marked comment #js {:sanitize true})}}]])
#'cljs.user/comment-component

NOTE 14: the dangerouslySetInnerHTML attribute expects a JS object as a value. See above for the use of the #js tagged literal.

Re-render the comment-box as usual

cljs.user> (render [comment-box] (by-id "content"))
#object[Constructor [object Object]]

Again, you immediately see the word another shown in italics

Italics

Not so bad.

Hook up the data model in React

This step requires some code refactoring. Instead of inserting the comments in the source code, we want to get them from a data structure. Eventually the data could come from a server.

Here is the new React code from the official tutorial:

var data = [
  {id: 1, author: "Pete Hunt", text: "This is one comment"},
  {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
];

NOTE 15: note the newly added id attribute.

Now, instead of manually instantiating the Comment components in the CommentList component, the CommentBox component has to get the comments from the data variable and pass them to CommentList as follows:

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
        <CommentForm />
      </div>
    );
  }
});

The code refactoring is not finished yet. First we need to redefine the CommentList in such a way that it receives the comments data from the CommentBox component as follows:

var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.data.map(function(comment) {
      return (
        <Comment author={comment.author} key={comment.id}>
          {comment.text}
        </Comment>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

The updated render function creates a new Comment node for each available comment contained in the data variable and finally returns the accumulated Comment components it created.

NOTE 16: note the newly defined key attribute getting the value from the comment id.

Finally we have to refactor the ReactDOM.render function as well, because the CommentBox instance now has to get the comments from the data variable as well to be able to pass them to the CommentList component.

ReactDOM.render(
  <CommentBox data={data} />,
  document.getElementById('content')
);

Reload locahost:3001. Even if the result is the same as before, you could potentially get the data from a web service.

Hook up the data model in Reagent

The very first step to hook up the data model in Reagent is as easy as defining a vector of maps resembling the array of objects from the JS data variable:

cljs.user> (def data [{:id 1
                       :author "Pete Hunt"
                       :text "This is one comment"}
                      {:id 2
                       :author "Jordan Walke"
                       :text "This is *another* comment"}])
#'cljs.user/data

Then we need to refactor the comment-list function definition to receive a list of comments as argument and then to call the comment-component function for each comment of the list.

cljs.user> (defn comment-list [comments]
             [:div
               (for [{:keys [id author text]} comments] 
                 ^{:key id} [comment-component author text])])
#'cljs.user/comment-list                 

The above very succinct code uses a powerful destructing ClojureScript idiom within a for macro call, known as list comprehension.

From each comment in comments, the {:keys [id author text]} destructuring form extracts the values of the :id, :author and :text keys and assigns them to the id, author and text symbols. Those values are then passed to the comment-component function to create a new comment. Note that we also used the ^ macro character to associate the {:key id} metadata map to each hiccup [comment-component author text] vector. This is required by the underlying React lib when you deal with dynamic children.

Next we have to consequently update the comment-box component to pass the comments data to the newly defined comment-list component.

cljs.user> (defn comment-box [comments]
             [:div 
              [:h1 "Comments"]
              [comment-list comments]
              [comment-form]])
#'cljs.user/comment-box

We are now ready to render the comment-box component into the "content" div of the reagent.html page. This time we have to pass the data vector to the comment-box so that comment-list can dynamically generate each comment in the data vector.

cljs.user> (render [comment-box data] (by-id "content"))
#object[Constructor [object Object]]

Hopefully, even if you do not see any difference in the rendered page, you should appreciate its improved dynamism.

Save your interactive experience

I suggest you to now stop the node server.js, the boot -c and the boot dev processes and then freeze into the example.js JSX file and into the reagent.cljs file the components you created in this tutorial.

Here is the content of the example.js file

var data = [
  {id: 1, author: "Pete Hunt", text: "This is one comment"},
  {Id: 2, author: "Jordan Walke", text: "This is *another* comment"}
];

var Comment = React.createClass({
  rawMarkup: function() {
    var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
    return { __html: rawMarkup };
  },

  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={this.rawMarkup()} />
      </div>
    );
  }
});

var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.data.map(function(comment) {
      return (
        <Comment author={comment.author} key={comment.id}>
          {comment.text}
        </Comment>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox data={data} />,
  document.getElementById('content')
);

An here is the complete content of the reagent.cljs file.

(ns modern-cljs.reagent
  (:require [reagent.core :as r]
            [cljsjs.marked]))

(def data [{:id 1
            :author "Pete Hunt"
            :text "This is one comment"}
           {:id 2
            :author "Jordan Walke"
            :text "This is *another* comment"}])

(defn comment-component [author comment]
  [:div 
   [:h2 author]
   [:span {:dangerouslySetInnerHTML 
           #js {:__html (js/marked comment #js {:sanitize true})}}]])

(defn comment-list [comments]
  [:div
   (for [{:keys [id author text]} comments] 
     ^{:key id} [comment-component author text])])

(defn comment-form []
  [:div "Hello, world! I'm a comment-form"])

(defn comment-box [comments]
  [:div 
   [:h1 "Comments"]
   [comment-list comments]
   [comment-form]])

Next, to be prepared for part 2 of the Reagent tutorial, I suggest you commit your work on both repositories:

cd /path/to/react-tutorial
git commit -am "Part I"
cd /path/to/modern-cljs
git add --all
git commit -m "Part I"

Next Step - Tutorial 22 - A reasoned porting of the Official React Tutorial to Reagent (Part II)

In Part II of this tutorial on Reagent we're going to complete our porting of the official React Tutorial to Reagent, by introducing components state management.

License

Copyright © Mimmo Cosenza, 2012-16. Released under the Eclipse Public License, the same as Clojure.