How to use React Lifecycle Methods in Re-frame

From OO to Clojure Workshop!
Watch my free workshop to help you learn Clojure faster and shift your paradigm to functional.
React's Lifecycle Methods can get really complex. They exist because not every component fits into the "functional view" abstraction. 90% of the time, you won't need these, but there are times when you need to manipulate the DOM directly. Even though the Lifecycle Methods are easier in Re-frame, there are still a few things to know.
This guide will teach you everything you need to know when you're developing a component that is just a little bit more complicated than you need 90% of the time. If you've got to hook into the REAL DOM elements, this guide is for you.
Table of Contents
- What are lifecycle methods?
- A refresher on Form-3 components
- The Lifecycle Methods we use in Re-frame
- Lifecycle Methods you probably don't need
- Deprecated Lifecycle Methods
- Conclusions
More posts in this Re-frame Series
- State in Re-frame
- The Re-frame Building Blocks Guide
- Guide to Reagent
- React Lifecycle for Re-frame ← you are here
- Re-frame Database Best Practices
- 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 are lifecycle methods?
React components have ten Lifecycle
Methods. These
are called by React on your components when various events
happen to that component. For instance, when the component
is first created, but before the DOM element is mounted into
the DOM, there is a method called componentWillMount()
. If
you implement that method, React will make sure it gets
called at the right time.
The lifecycle methods are how JavaScript programmers build in the stateful and effectful aspects that they put into components. For example, a JavaScript programmer might create a component that represents a user's account page in a dashboard. When the component is first created, it will fetch the data it needs from the server.
The React Lifecycle methods make sense. They are exactly the rope you need to to climb to the top of the hill. What's more, they have been refined over time. React's Lifecycle Methods have been used and abused a lot, and they are converging on the right set. However, there is still way more rope than you need to get tangled up in a mess of effectful spaghetti. I've seen it on React projects. It's what people hate the most about React because it's the part that is so easy to make a mess with.
We don't do that in Re-frame. In Re-frame, we separate out the data fetching from the components. We want our components to be, as much as possible, pure functions from input to DOM rendering. We still fetch data from the server, but we do it outside the components.
Reagent, which is used by Re-frame, gives you two methods for creating components that will serve you 90% of the time. Those are the Form-1 and Form-2 components that are based on functions. Form-1 is for components that only need state from the Database. Form-2 is for components that might also need local state.
However, for when you need to manipulate the DOM (the last 10% or less of components), you will need Form-3 components. Unfortunately, you're almost dropped down right into React and its Lifecycle Methods when you go to Form-3. I say "almost" because it may look like you're thrown to the wolves, even if you're not. In Re-frame, you really only need four of the Lifecycle Methods, and it's really clear when you need each one.
A refresher on Form-3 components
In Re-frame, when we want access to lifecycle methods, we need to use a Form-3 component.
A Form-3 component takes this basic shape:
(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
;; see https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle
;; for a complete list
;; 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])})))
A Form-3 component is a function that returns the value of a
call to
reagent.core/create-class
. create-class
takes a map of the Lifecycle Methods you'd like to
implement. The only one that is required is
:reagent-render
, which is the explicit form of the render
function you're used to in the other two Forms. You should
also pass in a :display-name
with the name of the
component in a string. That will help you debug, because the
name will be printed out in warnings and other console
messages.
The Lifecycle Methods we use in Re-frame
:component-did-mount
The :component-did-mount
method is called just once right
after the component is mounted into the DOM. This is the
first time that you will have access to the actual DOM
element connected with this component. The first and only
argument to this function is the component itself. You can
call
reagent.core/dom-node
on the component to get the DOM node and do whatever you
need to with it.
Example: CodeMirror IDE component
Why would you implement this method? Well, let's say you wanted to
embed a component that is not a React component into your page. For
instance, you wanted to use CodeMirror or
some other editor. CodeMirror asks you to construct a CodeMirror
object with a DOM node as an argument. It will then embed the editor
right into the DOM, basically unmanaged by React. As long as you don't
re-render, that editor will still be in the DOM.
(defn code-mirror []
(reagent/create-class
{:component-did-mount
(fn [comp]
(js/CodeMirror. (reagent/dom-node comp)))
:reagent-render
(fn []
[:div])})) ;; this div will get returned by `dom-node` above
Example: HTML Canvas component
You can also use this method to draw. Let's say your render method renders an HTML canvas. You can put the draw methods in here.
(defn html-canvas []
(reagent/create-class
{: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
(fn []
[:canvas])}))
Note: we used :component-did-mount
in the Externally Managed Components lesson of Building Re-frame Components to make a component out of the CodeMirror editor.
:reagent-render
This is the render method you normally create with a Form-1 or Form-2
component. It's a function that returns Hiccup and it includes all of
the Reagent magic to let it re-render when the arguments change or a
Subscription it deref
s changes. This one is required, or how else
would you render HTML?
Example: Form-1 convered to Form-3
A Form-1 component might look like this:
(defn person-info [id]
(let [person @(rf/subscribe [:person id])]
[:div
[:div.name (:name person)]
[:div.email (:email person)]]))
That same component, as a Form-3 component, would be written like this:
(defn person-info [id]
(reagent/create-class
{:display-name "person-info"
:reagent-render
(fn [id]
(let [person @(rf/subscribe [:person id])]
[:div
[:div.name (:name person)]
[:div.email (:email person)]]))}))
:component-did-update
The :component-did-update
method is called just after
re-rendering. That means that the DOM nodes are potentially
totally re-freshed from the return value of the render
method. You shouldn't trouble yourself with what has
changed. That's React's job. Just assume that you'll have to
redo everything you did in :component-did-mount
again.
Example: HTML Canvas Component
For instance, you can redraw that square you drew in the
:component-did-mount
method. That is a very common
pattern.
:component-did-update
(fn [comp]
(let [node (reagent/dom-node comp)
ctx (.getContext node "2d")]
(.fillRect ctx 10 10 10 10)))
:component-will-unmount
The :component-will-unmount
method is called just before
your component is stripped out of the DOM and thrown
away. You can use this to clean up anything you've
created. For instance, if you needed to register some event
handlers in that CodeMiror editor you embedded, this would
be the place to unregister those events.
Note that in JS React, a lot of Components will register
global events like window.resize
. Then they need to
unregister them here. Or they will do a lot of
window.setTimeout
s in the component and they'll need to
cancel them. In Re-frame, you won't be doing a lot of that
stuff. If you need to respond to resize
events, or other
events outside of this component, those should be stored in
an Atom or dispatched as an event in response and store the
width and height in the database.
Even though most cases are covered by standard Re-frame, still, anything the component has done to the DOM may need to be undone, so you might need this.
Lifecycle Methods you probably don't need
Okay, so I've said that you only need a few of the methods React gives you. I'm also going to go through the all of the ones that you won't need. I'll say why you don't need them. But I would like to explain them in that off chance that you really do need them. No one can know what you'll need until you need it.
constructor()
React components are defined in classes, and so have a
constructor. The constructor is used mainly to initialize
local state and other needed values. React gives many rules
and guidelines for what should and shouldn't be done in your
constructor, and for good reason. The component is still
getting set up, it has to call super()
constructors,
etc. It's a lot to remember and can result in some difficult
bugs.
Luckily, in Re-frame, we don't need to worry about that. We don't define a separate constructor. Instead, we can add things to the body of the function that represents our component, outside of any of the defined methods. This is where we initialize component-local state.
(defn stateful-component [id]
(let [local-state (rf/atom {})] ;; initialize state
(reagent/create-class
{:display-name "stateful-component"
:reagent-render
(fn [id]
[:div
(:count @local-state "Empty")])})))
VERDICT: you don't need it. Use the function itself to initialize any local state.
shouldComponentUpdate()
The shouldComponentUpdate()
method is called to ask the
component whether it thinks it should re-render. It looks to
React like something has changed. This is the component's
chance to check to see if the changes actually need to be
re-rendered.
A lot has been written about how to do this calculation quickly and save renderings. But guess what! Reagent has a default implementation for this method, and it does this really well. Probably better than you could do.
Because ClojureScript uses immutable data, it's really easy to know when things have changed. If you're comparing the old arguments and the new arguments, if they are the same object, nothing has changed, because that object is immutable. That one check is so quick and saves so many renderings, you're already ahead of the game.
VERDICT: leave Reagent's default implementation; it's fast enough.
getDerivedStateFromProps()
The getDerivedStateFromProps()
method is called right
before the render method. It is used to update the state
from the props. This is rarely used in React applications,
and I can't think of a reason to use it in Re-frame. We also
don't use state in the same way.
VERDICT: ignore it.
getSnapshotBeforeUpdate()
The getSnapshotBeforeUpdate()
method is called after the
render method returns but before the DOM is changed. The
docs suggest it might be useful to get the scroll position
before the DOM changes so you can adjust afterwards.
VERDICT: you may need this if you are doing fine-tuning of components to account for scoll position bugs or focus bugs.
getDerivedStateFromError()
The getDerivedStateFromError()
method is called if a
subcomponent throws an error. You get a chance to save the
error so that you can do something differently on the next
render. Remember, in JavaScript React, people use components
to do the work of the app.
In Re-frame, we rarely have need for a component to catch an error. Yes, we can have errors, but those are bugs, and should be fixed. This method is used in React to make components that wrap a failing component and show something else like a fail whale. However, since we tend to separate the view from the work of our app, in Re-frame, we don't need this. We also don't use component state in the same way.
VERDICT: ignore this.
componentDidCatch()
The componentDidCatch()
method is also called if a
subcomponent throws an error. However, this one isn't for
changing the state. Side-effects are allowed, so it's useful
for logging errors.
VERDICT: if you need to log errors, use it. Otherwise, ignore it.
Deprecated methods
React 16 has seen several older methods be deprecated. That means they still work but shouldn't be used for new code. I won't go over those here. Just know that they are there.
Conclusions
Re-frame gives you a lot of structure for your frontend application. You should use it. As functional programmers, we tend to want to separate out the rendering from the state updating. So we treat React like a pure, functional View. However, we can't avoid some state and effects. We tend to reach out from within our functional bubble when we need to go outside of React and modify the DOM directly.
React provides for this with its Lifecycle Methods. There is one at each stage of a component's life. However, most of them are uninteresting to a Re-framer. We have a place for state (the Database), we have a place for Effects, and we have a place for preparing the data components will need (Subscriptions). Use those, keep your components simple, and relax :)
The ones that are interesting directly relate to updating the DOM at certain strategic points: the first time the DOM is rendered, after each re-render, and when the component is removed from the DOM. Other than that, ClojureScript, Reagent, and Re-frame have your back.