A guide to the React Lifecycle Methods for Re-frame

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. However, for that last 10%, with Form-3 components, you're almost dropped down right into React and its Lifecycle Methods.

The React Lifecycle makes sense. It's exactly the rope you need to to climb to the top of the hill. However, it's also 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.

Luckily, we are functional programmers. We don't like mixing in our state changes and ajax requests in our components. And since we don't have to even touch that part of React 90% of the time, we're protected from the worst offenses. Let me just give an example of what people do with React: they fetch data from the server from within their components. They treat loading certain components into the DOM kind of like a page load that now requires a bunch of data from the server. It means that when I want to load a UserView component, it pass it the UserID in the constructor and it will go and fetch the data from the server when it loads into the DOM. What if I already have the user data? How do I test that?

In ClojureScript, and certainly in Re-frame, we tend to separate out those things. The UserView component might take a UserID to build it, but something else is going to be fetching that user data and putting into the Database. The component should not know where the data came from. That's basically how 90% of the cases are eliminated--basic functional programming.

But that last 10% need Lifecycle Methods. There are still stateful things happening to our components. For instance, before mounting, there simply is no DOM element. If you need to access the DOM element, you have to wait until after mounting. We need to hook into React Lifecycle Methods.

Luckily, we can simplify those methods a lot. The React Documentation lists ten Lifecycle Methods. In practice, in Re-frame, we only ever use four of those. Let me go over in detail each of those four. I'll go through each one, saying when to use it and how.

Form-3 components

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
       :render-reagent
       (fn [a b c]
         [:div {:class c}
           [:i a] " " b])})))

It's a function, like the other Forms. It gives you an opportunity to initialize some local state. But instead of returning Hiccup (like Form-1) or a function (like Form-2), it returns the value from a call to reagent.core/create-class. That function takes a map of the Lifecycle Methods you'd like to implement. The only one that is required is :render-reagent, which is 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. Here are the other Methods:

:component-did-mount

This 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.

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.

:component-did-mount
(fn [comp]
  (js/CodeMirror. (reagent/dom-node comp)))

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.

:component-did-mount
(fn [comp]
  (let [node (reagent/dom-node comp)
        ;; some canvas stuff
        ctx (.getContext node "2d")]
    (.fillRect ctx 10 10 10 10)))

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?

:component-did-update

This 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. For instance, you can redraw that square:

:component-did-update
(fn [comp]
  (let [node (reagent/dom-node comp)
        ctx (.getContext node "2d")]
    (.fillRect ctx 10 10 10 10)))

:component-will-unmount

This 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, dispatch an event in response and store the width and height in the database. Even though most cases are covered, still, anything the component has done to the DOM may need to be undone, so you might need this.

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.

:component-will-mount

This method is called right after the constructor but just before mounting into the DOM. It's an awkward part of the life of a component: the component has already been set up, but it still doesn't have a DOM node. What would you do here? You've already had a chance to set up some state. And you've still got to wait for that DOM node. Well, the answer is that even JavaScript Reacters don't recommend doing anything here. Re-framers should be setting stuff up in the outer function of Form-3.

VERDICT: ignore it.

:component-will-receive-props

This method is called when the arguments to your component are changed in the parent. In JS React, this is where you'd check on stuff like whether you want to fetch some new user data from the server. The UserID could have changed.

In general, Reagent wraps this up in such a nice package, you're not going to need this. If you need data from the server, use a subscription and save the data in the database. The component will update cleanly and easily as needed.

VERDICT: ignore it; use the database and subscriptions for data from the server.

:should-component-update

This 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 is 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.

:component-will-update

This method is called after React checked if the component should update and the component said yes. Now is the component's chance to get stuff ready for the upcoming re-render. But what preparations will you make? You should be doing serious calculations in Dynamic Subscriptions, not in the component. Doing that, you'll save even more unnecessary re-renders.

The fact is, I have never needed this method, I can't imagine needing this, but I also can't rule out its usefulness for some odd case I've never dealt with. So here's what you'll need to know. You won't know why you're updating: is it an argument change or a subscription change? If you need to, you can check if the arguments have changed. The new arguments are passed to your function and you can get the new ones with reagent.core/children.

:component-will-update
(fn [comp [_ & [a b c :as new-args]]]
  (let [old-args (reagent/children comp)]
    (when (not= new-args old-args)
      (js/console.log "Args have changed"))))

VERDICT: I seriously doubt you'll need it; calculate anything you need in Dynamic Subscriptions.

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.

Get on the mailing list!