How to use React Lifecycle Methods in Re-frame

Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.

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

More posts in this Re-frame Series

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 derefs 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.setTimeouts 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.

More posts in this Re-frame Series