-
Notifications
You must be signed in to change notification settings - Fork 12
Step 10: stateful components
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.
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.
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:
- go to the page of some phone, e.g http://localhost:3000/#/phones/droid-2-global-by-motorola
- 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.
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])))
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.
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.