-
Notifications
You must be signed in to change notification settings - Fork 397
Reagent Implementation
Purely Functional Guide to Reagent
The legacy OM code often calls directly into the app-state
or game-state
atoms which are now Reagent atoms. Thus when the state of these atoms changes we will re-render those components. This is really the same as what OM was doing already (i think). Future work will be to optimise this re-rendering but initial migration was a forklift w/o attempting to solve everything at once.
- Client Side Routing via Secretary. We currently render all parts of the site all the time. We could avoid doing this for screens the user is not currently in. This needs some consideration as some
go
loops are in namespaces we would not be rendering so would need to check these don't break. It should help performance. - Rendering optimisations
- Reduce or kill jQuery. jQuery is seen as something of an anti-pattern in React as state should flow down from the parent to components and probably things I don't understand too. We have a lot of it so best to chip away at...
- Explore if ref swap! can be moved out of our render functions. Documentation shows we are doing it the right way but it seems a bit wrong to do this every render for a static DOM node.
- Reduce use of React lifecycle functions. Reagent documents state some of the use of these is not usually needed and their are more Clojur-isms to handle most. Given the fork-lift migration and time/sanity/knowledge gaps these were not all tackled. Though were reduced ;)
In most cases component local state has been bound to a Reagent atom called s
. This is often passed to child components which need to update some state in the parent. In the old code this was mostly done using core.async
Example of this initial state setup in the outer Reagent function
(defn msg-input-view [channel]
(let [s (r/atom {})]
(fn [channel]
[:form.msg-box {:on-submit #(do (.preventDefault %)
(send-msg s channel))}
The legacy OM code base used the old version of React refs which were strings. During migration it was seen that consumers of the ref such as jQuery did not always "find" the DOM node. A new approach was used which adds the DOM node to a a clojure atom and it can be called from there.
Example of a ref binding now - which binds the DOM node into the chat-state atom
(defn message-view [message s]
(when (not my-msg)
[:div.panel.blue-shade.block-menu
{:ref #(swap! chat-state assoc :msg-buttons %)}
This binding can then be reffed from elsewhere in the app - for example:
(defn- hide-block-menu []
(-> (:msg-buttons @chat-state) js/$ .hide))
Typically refs have been bound to a component local state atom, though in some cases if the parent is many layers higher, or the code hard to read - the binding is to a name-space global atom.
Fragments in React Hiccup Support for Fragments
Long and short:
[:<> stuff1 stuff2 ]
This has been used as a wrapper to help out in places where it was not easy to give a :key to list items. Ract loves those.
In the Reagent app, app-state is a Reagent atom. When a reagent atom is updated is causes any components using it in a render method to render. Some cases where you want to be careful...
In our gameboard namespace we have this code which causes audio to play after a render:
(defn gameboard []
{:component-did-update
(fn []
(update-audio {:sfx (:sfx @game-state) :sfx-current-id (:sfx-current-id @game-state)
:gameid (:gameid @game-state)} soundbank))
This calls the update audio code - can you see a problem in the old snippet here?
(defn update-audio [{:keys [gameid sfx sfx-current-id]} soundbank]
(swap! app-state assoc :sfx-last-played {:gameid gameid :id sfx-current-id}))))
So update-audio
makes a swap!
on the app-state
atom ... this causes a re-render on the parent component which causes the component-did-update to fire... and then calls right back into update-audio. We have a re-rendering loop! Simple fix... don't use a r/atom to record this stuff.
(defonce sfx-state (atom {}))
(defn update-audio [{:keys [gameid sfx sfx-current-id]} soundbank]
(swap! sfx-state assoc :sfx-last-played {:gameid gameid :id sfx-current-id}))))
This might have cost me an hour ... there is something wrong in this component:
(defn build-hand-card-view
[player remotes wrapper-class]
(let [side (get-in @player [:identity :side])
size (count (:hand @player))]
(map-indexed
(fn [i card]
[:div {:class (str
(if (and (not= "select" (get-in @player [:prompt 0 :prompt-type]))
(= (:user @player) (:user @app-state))
(not (:selected card)) (playable? card))
"playable" "")
" "
wrapper-class)
:style {:left (* (/ 320 (dec size)) i)}}
(if (or (= (:user @player) (:user @app-state))
(:openhand @player)
(spectator-view-hidden?))
[card-view (assoc card :remotes remotes)]
[facedown-card side])])
(:hand @player))))
What could it be? It returns a list of divs. This gives this error in React which baffled me:
Uncaught Error: Objects are not valid as a React child (found: object with keys {ns, name, fqn, _hash, cljs$lang$protocol_mask$partition0$, cljs$lang$protocol_mask$partition1$}). If you meant to render a collection of children, use an array instead.
The fix? Wrap it in a [:div]!
(defn build-hand-card-view
[player remotes wrapper-class]
(let [side (get-in @player [:identity :side])
size (count (:hand @player))]
[:div
(map-indexed
(fn [i card]
[:div {:class (str
(if (and (not= "select" (get-in @player [:prompt 0 :prompt-type]))
(= (:user @player) (:user @app-state))
(not (:selected card)) (playable? card))
"playable" "")
" "
wrapper-class)
:style {:left (* (/ 320 (dec size)) i)}}
(if (or (= (:user @player) (:user @app-state))
(:openhand @player)
(spectator-view-hidden?))
[card-view (assoc card :remotes remotes)]
[facedown-card side])])
(:hand @player))))
]