ClojureScript + Reagent Tutorial with Code Examples
This guide will teach you all of the things that Re-frame gives you that we use in Re-frame. Re-frame is built on Reagent and uses many of the concepts from Reagent as is. Reagent is a thin but high-leverage wrapper around React.
In this guide, you will learn the three ways to create components, the details of Hiccup (including shortcuts), how to get access to the real DOM nodes, how to create atoms and react to changes in them, and how to understand re-rendering.
Table of Contents
- What is Reagent?
- Components
- Hiccup generates HTML
- Basic HTML
- Style attributes
nil
andfalse
attribute values- Other values
class
patternid
andclass
shortcut- Nested tag shortcut
- React DOM Events
- Sub-components
- JS React components
- Embedding HTML
- Node lists and keys
- Lazy seqs
refs
Getting at DOM nodes- Forms A best practice and a recommended pattern
- Reagent Atoms
- Understanding re-rendering
More posts in this Re-frame Series
- State in Re-frame
- The Re-frame Building Blocks Guide
- Guide to Reagent ← you are here
- React Lifecycle for Re-frame
- Database Structure in Re-frame
- Re-frame, a Visual Explanation
- Optimistic Update in Re-frame
- Timeout Effect in Re-frame
- Why Re-frame instead of Om Next
- 6 things Reacters do that Re-framers avoid
What is Reagent?
Reagent is a ClojureScript wrapper around React. It gives you a way to easily create React components. Reagent has three main features that make it easy to use: using functions to create React components, using Hiccup to generate HTML, and storing state in Reagent Atoms. Reagent lets you write React code that is concise and readable.
Reagent components are functions
Form-1 component
When we're using React from ClojureScript, we are typically
using React in a functional programming way. That is, we
want to define a function from inputs to DOM tree. Reagent
makes this as easy as possible. Just define a function that
returns Hiccup. This is called a Form-1 component. The
function corresponds to the React render()
method.
(defn green-button [txt]
[:button.green txt])
Form-2 component
The next case is slightly more complicated. If you need to
initialize something when the component is created, you use
a Form-2 component. Form-2 components are functions that
return their render function. The outer function will be
called once to get the render function. Then the render
function will be called each time the component needs to
render. If you wrap that inner render function in a let
,
you can initialize some state in there.
(defn counting-button [txt]
(let [state (reagent/atom 0)] ;; state is accessible in the render function
(fn [txt]
[:button.green
{:on-click #(swap! state inc)}
(str txt " " @state)])))
The outer function will be called once, then the inner function will be called for each render. The two functions should take the same arguments.
Form-3 component
React has a full lifecycle for its
components, which includes methods to initialize the
component and to perform effects when the component is
mounted. There are lots of methods for everything that
happens to a component from creation through removal from
the DOM. You rarely need access to those, but when you do,
Reagent gives you Form-3 components. Form-3 components are
functions that return a value created by
reagent/create-class
.
(defn complex-component [a b c]
(let [state (reagent/atom {})] ;; you can include state
(reagent/create-class
{:component-did-mount
(fn [] (println "I mounted"))
;; ... other methods go here
;; name your component for inclusion in error messages
:display-name "complex-component"
;; note the keyword for this method
:reagent-render
(fn [a b c]
[:div {:class c}
[:i a] " " b])})))
:display-name
gives the component a name which Reagent
will use when printing debugging messages. It's useful to
give it a name.
React defines nine different lifecycle methods. Each method has its own complex usage. Fortunately, Reagent makes most of the methods unnecessary. There are really only four you will ever need to use.
:component-did-mount
is called right after the component
is added to the DOM. This is the first time you will have
access to the actual DOM element connected with this
component. The method takes one argument: the component
itself. Call
reagent.core/dom-node
on it to get the DOM node. Implement this method to set up
DOM nodes for external components such as HTML canvas
es.
:component-did-mount
(fn [comp]
(let [node (reagent/dom-node comp)
;; some canvas stuff
ctx (.getContext node "2d")]
(.fillRect ctx 10 10 10 10)))
:reagent-render
is the render method you are used to from
Form-1 and Form-2. You simply need to give it a name in the
create-class
function. This method is required.
:component-did-update
is called just after
re-rendering. That means the DOM nodes have potentially just
been recreated by the Virtual DOM algorithm. If you are
doing something special with :component-did-mount
, you may
need to do it again here. You may also want to make some
changes based on changes to the local state.
:component-did-update
(fn [comp]
(let [node (reagent/dom-node comp)
ctx (.getContext node "2d")]
(.fillRect ctx 10 10 10 10)))
:component-will-unmount
is called just before the
component is unmounted from the DOM. You still have access
to the DOM node. This is your last chance to clean up
anything you might need to before your component is gone
forever. It is rare that you will want to implement this
method.
I go over each of them in more detail here.
See Creating Reagent Components for more explanation of the three forms.
Note: in general, Reagent will convert camelCase
(initial
capitals) into kebab-case
(lowercase with hyphens), which
is more common in Clojure. If you look up the documentation
for the React lifecycle methods, they will be noted in
camelCase
. You can mentally translate them to
kebab-case
.
Hiccup generates HTML
In normal React, you have two options for generating HTML in your render methods. The first and most common is to use JSX. JSX is a preprocessor to JavaScript that reads in HTML embedded in your JavaScript and turns it into normal React DOM method calls. The second method is to call those methods yourself directly, which is very verbose.
Luckily, Reagent provides a third (and possibly better) approach. Your render function can return Hiccup. Hiccup is a very common way to express HTML inside of Clojure applications. It may already be familiar to you.
Hiccup represents HTML elements as Clojure vectors. You specify the element type with a keyword. The HTML attributes go in a map. And the contents follow inside of the vector. The elements nest well. All-in-all, it is a very elegant way to embed HTML inside of Clojure.
Hiccup is more concise than using the React DOM method calls. But it is also more concise than embedded HTML since you omit the ending tag.
Basic HTML
The basics of Hiccup are fairly easy. If you have an HTML tag like
<div></div>
That becomes
[:div]
- Every HTML tag becomes a Clojure Vector. There's no need for a
closing tag because the Vector is closed with the
]
(closing square bracket). - The tag name,
div
, becomes a keyword with the same name,:div
.
HTML nests tags inside of other tags, so we'll need a way to do that.
<div>
<h1>Hello!</h1>
<p>This is my message.</p>
</div>
Becomes:
[:div
[:h1 "Hello!"]
[:p "This is my message."]]
- The
h1
is nested in thediv
. So the:h1
vector is an element of the:div
vector. It has to come after the tag name. You can have multiple children. - Text is represented as a ClojureScript String.
Luckily, React and Reagent handle nil
children gracefully. That lets
you do some convenient patterns like this:
[:div
(when title [:h1 title])
(when msg [:p msg])]
when
will return nil
if the test is falsy. If both title
and
msg
are missing, you get this hiccup:
[:div nil nil]
Reagent will ignore nil
s and render it:
<div></div>
Okay, but what about attributes? HTML tags usually have attributes, like this:
<div class="container">
<h1 id="greeting">Hello!</h1>
<p>This is my message</p>
</div>
In Hiccup, the attributes are a hash map that comes right after the tag name. This becomes:
[:div {:class "container"}
[:h1 {:id "greeting"}
"Hello!"]
[:p "This is my message"]]
The attributes hash map is optional. And note that the attribute names
are kebab-case
in Reagent. If you look through the React
documentation, they are in camelCase
. Also: class
is a reserved
keyword in JavaScript, so React refers to the className
attribute. In Reagent, that is correctly shortened back to :class
.
Hiccup has some special rules for some attributes and their values that are super convenient when building components.
Style attributes
In HTML, if you want to put an inline style in a tag, you do so with an attribute. The value is a string of valid CSS.
<div style="margin-left:10px; width:15%">
<p>Some text</p>
</div>
You're embedding a string in there that has to be correct CSS. But
when you're generating these attributes programmatically, you don't
want to have to concatenate strings properly using complex logic. You
want it broken out into something easier to work within your
language. Luckily, React did break the style
attribute out. In
React, you use a JavaScript object. The keys are the CSS property
names, and the values are the CSS values as strings. Numbers also work
for some properties. They become numbers of pixels. For instance,
200
becomes 200px
in CSS.
Reagent builds on that and lets you use a ClojureScript map to
represent inline styles. The same rules apply: the name of the CSS
property is the key and the value of the CSS property is the
value. Here's what the above div
with inline styles will look like:
[:div {:style {:margin-left 10
:width "15%"}}
[:p "Some text"]]
Notice we're using a number, 10
, because it's 10px
. But we have to
use the string "15%"
because the units are not px. Also, notice
that we can use the proper CSS name, margin-left
, which is not valid
JavaScript because of the -
. Reagent handles that for us!
React also gives us a few extra shortcuts that really help when generating these data structures.
nil
and false
attribute values
If the value of an attribute (any attribute, including styles) is
nil
or false
, the attribute is not included in the tag. This is
very useful when you're building something conditionally. Here's an example:
(defn message [txt urgent?]
[:div {:style {:color (when urgent? "red")}}
txt])
When urgent?
is true
, we want the message text to be red. But when
it's not true
, we don't want to change the color at all. This code
will do just that, because the when
expression will be nil
when
urgent?
is false
.
Some HTML attributes don't take on values. They're just supposed to be there or not. For instance, if you want a checkbox to be checked, you do:
<input type="checkbox" checked />
The checked
attribute doesn't need a value. But in our
representation, we're using a map. So every key needs a value! Reagent
and React are smart. You can use true
and false
as values. If you
want the checkbox checked, give the checked
attribute a value of
true
, otherwise, false
. No need to worry about having it or not.
(defn checkbox [name checked?]
[:input {:type :checkbox
:checked (boolean checked?)
:name name}])
Other values
ClojureScript values for attributes have their own semantics. Here's how they work:
- Booleans:
false
means ignore the attribute,true
means show it with no value. - Strings: passed along as-is
- Numbers: if they're the values of styles, they're
px
; otherwise, turned into Strings - Keywords and Symbols: their names (Strings) are passed;
:a/b
gets turned into"b"
- Maps: Turned into JavaScript Objects and passed to React.
- Other collections: Turned into equivalent JS values and passed along.
- Functions: passed along
nil
: ignore the attribute
class
pattern
A lot of HTML tags have multiple classes. Some of those classes are
permanent, meaning they don't change. But some of them will
change. For example, you might want to highlight the active element in
a list view. So you add the active
class, which gives is a blue
glow. But then as soon as it's not active, you remove that class. This
can get messy since you're often concatenating the strings manually.
My solution is to write a small helper function that will concatenate
them for me. It lets me write the logic inline. I call it cs
because
it's short. It stands for classes.
(defn cs [& names]
(str/join " " (filter identity names)))
The filter
gets rid of nil
and false
names. Then I join them up
with a space between them. I use it like this:
(defn element [txt active? hidden?]
[:div {:class (cs "element"
(when active? "active")
(when hidden? "hidden"))}
txt])
Perfect? I don't think so. But it works :) I'd love to hear what you do.
id
and class
shortcut
Reagent also has
shortcuts for id
and class
attributes. These
can be useful. Here's how it works.
Instead of setting the :id
attribute, you can simply put a #
symbol after the tag name. It's supposed to look like CSS selector
syntax.
[:div#main-content
....]
That creates HTML like this:
<div id="main-content">...</div>
If you don't have any other attributes, it's way shorter and easier to read.
You can do something similar with the :class
attribute. This time,
use a .
like in CSS:
[:button.btn.btn-primary ...]
Notice that that creates two classes:
<button class="btn btn-primary">...</button>
Also very convenient. You can use both the id and class shortcuts, but you should always put the id one first.
And the shortcut class does work with the class attribute! Consider putting the permanent classes in the shortcut and the temporary classes in the attribute.
Nested tag shortcut
Something that Reagent Hiccup does that I don't use very much is to allow you to deeply nest tags with a single vector. You use something quite like the CSS selector for directly nested tags. It seems like it could be very useful for some of those CSS frameworks that like to wrap everything up in divs with classes:
[:div.wrapper>div.inner
"Hello!"]
That creates this structure:
<div class="wrapper">
<div class="inner">Hello!</div>
</div>
It's great when that's all you need. But note that you can't set attributes on the outer elements, only the inner one.
React DOM Events
React defines its own events which wrap the regular DOM events
provided by the browsers. Each browser is different, so this is really
good for cross-platform coding. What's more, React's events are more
like how you'd expect things to work. For instance, the onchange
HTML Event on a text input fires when the element loses focus, not
when you're typing in it. React fixes this problem, and more.
In React, you set the event handlers with attributes. The values
should be functions. The functions take a single argument, which is
all the information about the event. I usually call it e
because
it's short.
All React event objects have some common properties and some
properties specific to them. The
React docs are the best source
of information on all of the possible events. It is complete and
up-to-date. Just remember that Reagent will use kebab-case
while the
React docs use camelCase
.
Just as an example, here's how we can respond to the :on-click
event
of a button:
[:button.btn.btn-primary
{:on-click (fn [e] (js/console.log "Hello!!"))}
"Click me!"]
Now a word about default handlers and event propagation. The HTML event model is still in effect. These are just HTML events with a small wrapper object around them.
You should be familiar with the event propagation model of the
DOM. Basically, when an event fires on a DOM node, the handler on that
node is called. Then the handler is called on its parent. Then on its
parent, all the way up to the top of the DOM tree. You can stop that
behavior in any handler by calling (.stopPropagation e)
(see
stopPropagation
).
Also, many elements have default behaviors. For instance, a submit
button will submit the form it is located in when clicked. You can
prevent the default behavior by calling (.preventDefault e)
inside
the handler. See preventDefault
.
Sub-components
Reagent adds an extra feature to Hiccup, which is to allow you to embed Reagent components directly into the Hiccup with no extra ceremony. Simply put the component's name in the place of the element name keyword and follow it by its arguments.
;; hiccup to render deeply nested div
[:div#login-form
[:form {:method :post :action "/login"}
[username-field] ;; embed Reagent component (defined elsewhere)
[password-field a b c] ;; note the arguments
[:button {:type :submit} "Log in"]]]
Notice how easy it is to close those things. No more trouble matching tags.
Embedding React components from JavaScript
There's another trick you can do with Reagent Hiccup. Sometimes you
have a React component, let's say it's written in JavaScript, and you
want to embed it in the Hiccup. There's a bit of translation that
Reagent will have to do. However, Reagent makes it easy. Use the :>
directive instead of an HTML tag name or a Reagent component.
[:> js/nativeJSComponent {:title "Hello" :otherProp "World"}]
After the :>
directive, you put the React component as it is named
in JavaScript. Then you follow it with the React component's
props. Note that the Clojure map is converted into the proper
JavaScript object, but camelCase
should be preserved.
When you have a string of HTML
Let's say you're making a component to show a blog post. The content of the blog post is a string containing HTML. So you want that HTML to render inside of the component. You might think to do something like this:
(defn blog-post [html]
[:div.blog-post
html])
But that won't work. If you just embed the string right in the component, all of the HTML tags will be escaped. You'll see the code.
React provides a way to set the HTML content of any element using a string. Here's how it looks:
(defn blog-post [html]
[:div.blog-post {:dangerouslySetInnerHTML {:__html html}}])
It's crazy. It's longwinded. It's verbose and a pain to type. And
that's on purpose. They
don't want you doing this by accident. You see, React is there to make
the DOM easier to use. And one of the things that make the DOM hard
is cross-site scripting attacks. If you render comments from blog
readers, and that content is not sanitized, you could render some HTML
with a <script>
tag in it that could do some nasty stuff to other
readers. So the default behavior of components is safe: escape the
string. Of course, sometimes you do trust the data and the data is in
HTML, so you want to just embed it. That's what this is for. Make sure
you really do trust that HTML (either by its source or it has been
sanitized) before you use this.
Node lists and keys
React lets you return embed an array of DOM elements inside of a parent element. The elements of the array become the children of the parent. In order to support a fast way to detect changes inside of this array, React suggests that you add a unique and stable key attribute to the elements of the array.
Similarly, Reagent's Hiccup lets you embed seq
s as children of other
nodes. These are converted to JavaScript arrays when the Hiccup is
converted to React DOM. When using seq
s of elements, you need to
provide keys as well. It's not strictly required, but it is good
practice. You'll see lots of warnings if you don't add a key.
The keys should be strings. They should be unique for that seq
. And
they should be stable. The key is your chance to tell React that this
element is the same as the one you rendered last time with the same
name.
Here's an example:
(defn student-list [students]
[:ul
;; for returns a seq
(for [student students]
[:li {:key (:id student)} ;; stable and unique key
[:a {:href (:url student)} (:name student)]])])
Lazy seqs
There's one more thing. In the above example, where I created a seq, I actually created a lazy seq. This is normally okay. No problem. But sometimes it's not okay. Check out this code:
(defonce student-urls (reagent/atom {}))
(defn student-list [students]
[:ul
;; for returns a lazy seq
(for [student students]
[:li {:key (:id student)}
;; deref the atom
[:a {:href (get @student-urls (:id student))}
(:name student)]])])
Well, this looks almost the same, except with one important
difference: you are deref
ing a Reagent Atom. Why does this matter?
Well, the elements of the lazy seq are not created right away. It's
lazy, so the elements are only created as they are accessed. That
means that this function will return before the body of the for
is
run. Since the function has already returned, the Atom doesn't know
which Reagent Component it was part of and it won't know to re-render
it when it changes.
The trick is to force the lazy seq to be created before you
return. Just wrap the lazy seq in a
doall
.
(defonce student-urls (reagent/atom {}))
(defn student-list [students]
[:ul
;; for returns a lazy seq
(doall
(for [student students]
[:li {:key (:id student)}
;; deref the atom
[:a {:href (get @student-urls (:id student))}
(:name student)]]))])
Getting at the DOM node itself
React provides a
great level of indirection
from the DOM itself. You no longer have to do everything by setting
properties and adding and removing children. However, there are times
when you really do want the individual DOM nodes. Some APIs, for
example, want the DOM node. The
FormData
API takes in the <form>
node. Or if you need to draw on a
<canvas>
, you need to get a reference to the actual DOM node.
React knows this is important so it has provided a really nice
facility to get the DOM nodes of any element you render. It's called
refs. Reagent makes it super easy to use. You'll want a Form-2 or
Form-3 component for this, because you'll want a local state. Just add
the :ref
property to the elements you want to keep track of.
(defn form-canvas []
(let [s (reagent/atom {})]
(fn [] ;; Form-2
[:div
[:form {:ref #(swap! s assoc :form %)} ;; save the FORM node
[:input {:type :text :name "first-name"}]
[:input {:type :hidden :name "token" :value "Hello!"}]]
[:canvas {:ref #(swap! s assoc :canvas %)}]]))) ;; save the CANVAS node
So what's going on here? In this component, we're rendering a form
and a canvas
inside of a div
. We want to keep a reference to the
form
and canvas
elements themselves so we can use them (which I've
chosen not to show, just for brevity). And notice we've added :ref
attributes. These take a function of one argument. That argument will
be the DOM node itself, or nil
in cases where the node doesn't
exist. We save it away safely in the state atom we're keeping.
Now, I've said that it's either the DOM node or nil
. That means
you'll have to check when you're ready to use it. When will it be
nil
? Either before the DOM node has been added to the DOM (before
mounting) or after the DOM node has been removed from the DOM (after
unmounting). This mechanism is nice because it makes cleanup
easier. React can call your ref
function with nil
and references
are cleaned up.
You can use those nodes you've saved however you like. Just be sure to
check if they're nil
before you call methods on them.
Forms
There is one very important pattern to use when building forms. In single-page apps, you often don't use the default form behavior of posting to the server. Instead, you capture the form submission and do something yourself, like your own AJAX request. Whatever you do, you will want to get the values out of the form, usually in a centralized place. Also, you want the form to be interactive—it's not just a box to fill in. Instead, you give feedback to the user as they type. You will want to centralize the local component state. Here's the pattern.
First, we make a Form-2 component that takes the default values in the form.
(defn contact-form [first-name last-name email-address message]
Then we create an Atom to store local state. We'll pass in the initial values.
(defn contact-form [first-name last-name email-address message]
(let [s (reagent/atom {:first-name first-name
:last-name last-name
:email email-address
:message message})]
Now we return the inner (render) function. Notice that we don't repeat the arguments. We don't want changes to those arguments to change the form after we have constructed it.
(defn contact-form [first-name last-name email-address message]
(let [s (reagent/atom {:first-name first-name
:last-name last-name
:email email-address
:message message})]
(fn []
Your form shouldn't do the default submission behavior. So prevent the default in the handler and do whatever you need. The current values you want will be in the Atom.
(defn contact-form [first-name last-name email-address message]
(let [s (reagent/atom {:first-name first-name
:last-name last-name
:email email-address
:message message})]
(fn []
[:form {:on-submit (fn [e]
(.preventDefault e)
... ;; do something with the state @s
)}
Now we create the input fields. Let's start with just one. We want to
lock the value of the input field to the Atom. To lock it, we need to
go both ways: if the Atom changes, we change the field. If the field
changes, we change the Atom. We set the value
attribute based on the
contents of the Atom. And our :on-change
handler swap!
s into the
Atom. We have to dig into the event, getting the .-target
(the DOM
element), then it's .-value
(the current text).
(defn contact-form [first-name last-name email-address message]
(let [s (reagent/atom {:first-name first-name
:last-name last-name
:email email-address
:message message})]
(fn []
[:form {:on-submit (fn [e]
(.preventDefault e)
... ;; do something with the state @s
)}
[:input {:type :text :name :first-name
:value (:first-name @s)
:on-change (fn [e]
(swap! s :first-name (-> e .-target .-value)))}]
Now, the value at :first-name
in the Atom will be locked to the
input field. Let's do the other fields:
(defn contact-form [first-name last-name email-address message]
(let [s (reagent/atom {:first-name first-name
:last-name last-name
:email email-address
:message message})]
(fn []
[:form {:on-submit (fn [e]
(.preventDefault e)
... ;; do something with the state @s
)}
[:input {:type :text :name :first-name
:value (:first-name @s)
:on-change (fn [e]
(swap! s :first-name (-> e .-target .-value)))}]
[:input {:type :text :name :last-name
:value (:last-name @s)
:on-change (fn [e]
(swap! s :last-name (-> e .-target .-value)))}]
[:input {:type :email :name :email
:value (:email @s)
:on-change (fn [e]
(swap! s :email (-> e .-target .-value)))}]
[:textarea {:name :message
:on-change (fn [e]
(swap! s :message (-> e .-target .-value)))}
(:message @s)]])))
The only difference on the last one is that it's a textarea
, so the
value is set as a child instead of the value
attribute.
What I like to do is store the state of all inputs at the level of the form. So there's a component for the form that controls everything. Why? For a number of reasons. The first is that when I do validation, I find that I often need access to the values in multiple fields. And for the sake of feedback to the user, I like to recalculate and display the validation with every keystroke. Also, when you're finally ready to submit the form, you have everything you need to build up the AJAX request or whatever it is you are doing with it. I've tried breaking up the elements into components, but I have trouble re-centralizing the values.
You can get more sophisticated than the example above. For instance, you might not want to show the validation until after they've blurred the field. You can keep track of "dirty" input fields inside the state as fields get blurred.
So that's the locking pattern with fields, where they get locked to an Atom. And there's my recommendation to build the component at the level of the form so you can centralize the state. And finally, you can add onto this recommendation to make more sophisticated interactions.
Reagent Atoms
Applications need state. Part of the magic of good libraries is how that state is managed. Reagent gives us a single, flexible tool for handling state, and it's one you're probably familiar with if you've programmed in Clojure before: Atoms.
Reagent has its own Atoms. They're just like regular Clojure
Atoms in every way except they do one extra thing:
they keep track of Reagent components who have deref
ed them. Every time those
Atoms change values, the component will be re-rendered.
What that means is that you have very "reactive" way to use state. You can create as many Reagent Atoms as you want. You can write any code you want to modify the states of the Atoms. And your Reagent components will re-render in response. Atoms provide one level of indirection between your app's behavior and how it is rendered.
There are four functions you will need to use the Atom API.
You create a Reagent Atom by calling
reagent/atom
.
After that, you can use
reset!
to set the
current value of the atom regardless of the current value.
(reset! state 10) ;; set state to 10
swap!
is the most
common way to change the value of an Atom. It calculates a new value
based on the current value, using a function you pass it.
(swap! state + 12) ;; add 12 to current state
We've already talked about
deref
. It returns the
current value of the Atom. There's a reader shortcut using the @
symbol as a prefix. The special thing about deref
for Reagent Atoms
is that the component doing the deref
ing will be re-rendered every
time the Atom's value changes.
(deref state) ;; returns current value
@state ;; equivalent to above
When will components be re-rendered?
It is very easy to understand when Reagent components will be re-rendered. In fact, there are only two circumstances under which a component will be re-rendered.
The first way is to change the arguments to the component as it is embedded in Hiccup. Here's an example. Let's say you had this Hiccup inside of a component.
[:div
[my-component a]]
my-component
is a component and a
is a local variable bound to
1
. Let's say that the component that returns this hiccup gets
re-rendered, but this time, a
is bound to 2
. my-component
will
be re-rendered, since its arguments have changed.
But what made the outer component re-render? Let's zoom out and look at that component.
(defonce state (reagent/atom [1 2]))
(defn big-component []
(let [[a b] @state]] ;; deref a reagent atom
[:div
[my-component a]]))
This big-component
deref
s a Reagent Atom in its render function. The Atom keeps track of
all of the components that do so. And whenever that Atom changes, all
of the components that deref
ed it up to that point are
re-rendered. That's the second way to re-render a component. Since
big-component
doesn't have any arguments, reacting to the changing
Atom must be how it got re-rendered.
WARNING
You should only deref
(or use @
) inside the render function, not in the outer scope of a Form-2 component. The following is wrong:
(defonce state (reagent/atom [1 2]))
(defn big-component-no-re-render []
(let [[a b] @state]] ;; deref a reagent atom
(fn [] ;; form-2 component returns a render fn
[:div
[my-component a]])))
This component will not re-render when state
changes. Why? Because the deref
does not occur inside the render function. Instead, it's only done once, when the component is initialized. This is a common mistake. Derefing should always happen in the render function.
So here's the trick: look for @
signs and arguments inside the render function. If any of those
change, the component will be re-rendered.
More posts in this Re-frame Series
- State in Re-frame
- The Re-frame Building Blocks Guide
- Guide to Reagent ← you are here
- React Lifecycle for Re-frame
- Database Structure in Re-frame
- Re-frame, a Visual Explanation
- Optimistic Update in Re-frame
- Timeout Effect in Re-frame
- Why Re-frame instead of Om Next
- 6 things Reacters do that Re-framers avoid