Skip to content

Step 10: stateful components

Valentin Waeselynck edited this page Sep 6, 2015 · 6 revisions

Browse code - Diff - Live demo

To reset your workspace for this step, run git checkout step-10.


Sometimes, it really only makes sense to have some state be local to a component, not global. We'll see how to make Reagent components which have local state, by adding a feature to the phone detail page: the ability to change the main picture.

Stateful components in React and Reagents

Reagent wraps React, and in React all components can be stateful out of the box. In React, you make a stateful component by implementing some lifecycle methods.

Reagent strives to make using React less tedious by hiding most of the lifecycle boilerplate from you. In Reagent, you make a stateful by writing not a rendering function, but a function returning a rendering function. The local state is simply locals of the outer function, over which the inner function closes.

The general recipe for stateful components is as follows:

(def <my-stateful-component> [x y]
  (let [local-state (r/atom ...)]
    (do-some-initialization-stuff-with local-state)
    (fn rendering-fn []
      [:div {:on-some-event #(manipulate! local-state)}
       [:div (content-using-value-of @local-state)]])))

You can find more examples of this on the project page of Reagent, and here as well.

First try

Using the method described above, let's give some local state to our phone-detail-cpnt:

(defn <phone-detail-cpnt> [phone]
  (let [{:keys [images]} phone
        local-state (rg/atom {:main-image (first images)})]
    (fn [phone]
      (let [{:keys [images name description availability additionalFeatures]
             ;; ....
             } phone]
    [:div
     [:img.phone {:src (:main-image @local-state)}]
     [:h1 name]
     [:p description]

     [:ul.phone-thumbs
      (for [img images]
        [:li [:img {:src img :on-click #(swap! local-state assoc :main-image img)}]])]
     
     ;; ...
     ]))))

If you go to some phone page, you can click on the images and see how the main image changes.

'Mission' accomplished, you say? Not so fast! There's a subtle bug here. Try the following procedure:

  1. go to the page of some phone, e.g http://localhost:3000/#/phones/droid-2-global-by-motorola
  2. then manually change the URL of your tab to that of another phone, e.g http://localhost:3000/#/phones/droid-pro-by-motorola

Notice anything? The main picture does not change when you go from one page to the other. In other words, the local state of the first page is still effective in the second page.

Second try: correct lifecycle management

Actually, the solution to the above problem is a one-line change in the parent component:

(defn <phone-page> [phone-id]
  (let [phone-cursor (rg/cursor state [:phone-by-id phone-id])
        phone @phone-cursor]
    (cond 
      phone ^{:key phone-id} [<phone-detail-cpnt> phone] ;; adding a :key property in the meta when calling our component
      :not-loaded-yet [:div])))

Explanation

This section is a little more advanced that the rest of the tutorial. Feel free to skip in at first.

What went wrong on the first try? We're facing a component lifecycle management issue here. Before our component gets mounted to the DOM, React performs the initialization of our state, in this case setting the initial value of the local-state atom. But when we're moving from one phone page to another, React's diffing algorithm does not see any difference: there was a phone-detail-cpnt node, and now there's still a phone-detail-cpnt node in the same place. React does not know we're dealing with another phone. For this reason, the initialization of the component is not performed when we want to.

Fortunately, there's an easy solution to this. React lets us add a key property to a component, as a hint to its diffing algorithm. By binding the id of the current phone to this key property, we can make React's diffing algorithm aware of when the phone changes. In Reagent, this can be done by adding a :key property to the metadata map of the form that invokes the component. This is exactly what we did in the above code.

Once this correction made, when we move from one page to another, the diffing algorithm can see that this is a different phone-detail-cpnt node. The initial phone-detail-cpnt node unmounts, a new one mounts and the initialization is performed again.

For more insights on this topic, see React's documentation for reconciliation.

Summary

Reagent helps us write components with very little tedium, but it is important to understand the lifecycle of such components :

  • To make a stateful component, you write it not as a rendering function, but as a function which returns a rendering function
  • the rendering function closes over some local state declared in the wrapping function
  • It is often useful to tie the lifecycle of stateful components using a :key metadata property, to make the component 'restart' under certain conditions.
  • Currently, we know how to make stateful components that initialize some local state, but not stateful components that need to clean up after themselves. We'll learn how to do that in Step 12 using React interop.

In the next step, we'll introduce asynchrony and error management using Communicating Sequential Processes.