How to use React Lifecycle Methods in Re-frame
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 s trategic 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.
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
- 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