Thanks for the suggestions.
So it sounds like in the my case the state is just a normalized store of
artists, albums, and songs. Some portion of the app state would look something
like:
{:artists [{:id 1
:name "The Beatles"}]
:albums [{:id 1
:artist-id 1
:name "Abbey Road"}
{:id 2
:artist-id 1
:name "Rubber Soul"}]
:songs [{:id 1
:album-id 1
:name "Come Together"}
{:id 2
:album-id 1
:name "Something"}
{:id 3
:album-id 2
:name "Drive My Car"}
{:id 4
:album-id 2
:name "Norwegian Wood"}]}
The :current-artist could actually then just reference artists/ablums/songs and
the various components could derive their state:
{:current-artist {:id 1
:albums [{:id 1
:songs [{:id 1}
{:id 2}]}
{:album-id 2
:songs [{:id 3}
{:id 4}]}]}}
Similarly, the player component could derive state from references in the
:playlist portion of the app state:
{:play-index 2
:songs [{:id 1}
{:id 2}
{:id 3}
{:id 4}]}
Am I on the right track here?
As the user navigates around to various artist pages there will be API requests
being made and new artist/album/song data will be added to the "store" I
mentioned above. I think the hard part now would be knowing when data is no
longer needed and can be purged from the store. Any advice for this? I'm
imagining the need to scan through the store and delete items that are not
referenced by the :current-artist or :playlist. As other areas of the
application begin to reference this shared data the cleanup process would
become more and more coupled to those areas.
On Monday, February 16, 2015 at 6:20:11 AM UTC-5, Oliver George wrote:
> Hey Leon
>
>
> It was really a matter of practicalities.
>
>
> There doesn't seem to be hook to inject derived state into OM cleanly just
> now. I have tried various other approaches and my code was much less
> managable. This approach is proving really convenient.
>
>
> One really strong win with this approach is that derived state happens before
> OM snapshots state to decide what's changed. Before I had this I had the
> issue of having to manually observe many paths to ensure that components
> updated. Now it just happens. You might need to have experienced the pain
> to fully appreciate that. It's big.
>
>
> The change to OM would be small but I figure it's better to prove the
> approach in some code rather than ask for extensions. I think the change
> would be either a new interface so that OM can derive state when fetching the
> atom's value. Somewhere around here is the deref I needed to inject my
> derived state.
>
>
> As for memoize-last. Well, memoize seems like a memory leak which worried me
> in a browser... no way to clear out old cached info. CLJS doesn't seem have
> a LRU memo function just yet (at least I couldn't get core.memoize to work).
> memoize-last just happens to fit perfectly with this particular use case.
>
>
>
>
> On Mon, Feb 16, 2015 at 9:16 PM, Leon Grapenthin <[email protected]> wrote:
> Can you elaborate why you implement a new IDeref? Why not just calculate and
> pass the derived state during rendering? Memoization in any fashion could
> still happen.
>
>
>
> Also, is memoize-last just intended as an optimzation to consume less memory,
> or does it serve another purpose?
>
>
>
> On Sunday, February 15, 2015 at 11:58:17 PM UTC+1, Oliver George wrote:
>
> > My approach to this stuff is to think in terms of state and derived state.
>
> >
>
> >
>
> >
>
> > I have a few helpers to facilitate implementing that which amounts to
>
> > Use an atom for stateUse a function to derive stateUse a simple cache to
> > reduce load of derivation in render loopHack deref to make atom pass back
> > derived-state
>
>
>
> > If you're new to OM I'd encourage you not to get carried away with picking
> > up dodgy bits of code like the ones I'm sharing here until you're confident
> > you're making best use of OM as it comes out of the box.
>
> >
>
> >
>
> > That being said...
>
> >
>
> >
>
> > Here's my derived-atom! function.
>
> >
>
> >
>
> >
>
> >
>
> > (defn derived-atom!
>
> >
>
> >
>
> > "
>
> >
>
> >
>
> > This allows us to access derived values associated with atom state.
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> > The implementation of atom does not use deref so we aren't interfering
>
> >
>
> >
>
> > with how state transitions, they use .-state to access the pure attribute
>
> >
>
> >
>
> > value.
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> > The implementation of om ref cursors use deref to resolve the current
> > value
>
> >
>
> >
>
> > of state so we are able to apply our logic and have the result available
>
> >
>
> >
>
> > via (value c) which means reference cursors will trigger updates based
>
> >
>
> >
>
> > on changes to derived data too.
>
> >
>
> >
>
> > "
>
> >
>
> >
>
> > [iref derived-fn]
>
> >
>
> >
>
> > (specify! iref
>
> >
>
> >
>
> > IDeref (-deref [_] (derived-fn (.-state iref))))
>
> >
>
> >
>
> > iref)
>
> >
>
> >
>
> >
>
> > This is how i used it as my om app-state
>
> >
>
> >
>
> >
>
> >
>
> > (defonce app-state (derived-atom! (atom {}) (memoize-last derived-state)))
>
> >
>
> >
>
> >
>
> > memoize-last is a cutdown version of memoize which just remembers the last
> > version. That essentially means that we only calculate derived-state once
> > for each OM state
>
> >
>
> >
>
> >
>
> >
>
> > (defn memoize-last
>
> >
>
> >
>
> > "Returns a memoized version of a referentially transparent function.
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> > The memoized version of the function keeps a cache of the *most recent
> > call*
>
> >
>
> >
>
> > from arguments to result.
>
> >
>
> >
>
> > "
>
> >
>
> >
>
> > [f]
>
> >
>
> >
>
> > (let [mem (atom {})
>
> >
>
> >
>
> > lookup-sentinel (js-obj)]
>
> >
>
> >
>
> > (fn [& args]
>
> >
>
> >
>
> > (let [v (get @mem args lookup-sentinel)]
>
> >
>
> >
>
> > (if (identical? v lookup-sentinel)
>
> >
>
> >
>
> > (let [ret (apply f args)]
>
> >
>
> >
>
> > (reset! mem {args ret})
>
> >
>
> >
>
> > ret)
>
> >
>
> >
>
> > v)))))
>
> >
>
> >
>
> >
>
> > Finally, here's me using derived state logic to do things like lookups,
> > validation and other things. It's all very app specific but amounts to
> > doing (assoc-in state [:a :b] :something) a lot. At this risk of confusing
> > things here's my generalised required field validation logic which is
> > implemented as derived state.
>
> >
>
> >
>
> >
>
> >
>
> > (def empty-values #{nil "" [] {} #{}})
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> > (defn validate-required-field [field]
>
> >
>
> >
>
> > (let [{:keys [required value errors]} field]
>
> >
>
> >
>
> > (if (and required (contains? empty-values value))
>
> >
>
> >
>
> > (assoc field :errors (conj errors "This field is required"))
>
> >
>
> >
>
> > field)))
>
> >
>
> >
>
> >
>
> >
>
> >
>
> > (defn is-required-field?
>
> >
>
> >
>
> > "Identifies walker nodes which are fields relevant to require logic"
>
> >
>
> >
>
> > [m]
>
> >
>
> >
>
> > (and (map? m)
>
> >
>
> >
>
> > (contains? m :required)
>
> >
>
> >
>
> > (contains? m :value)))
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> > (defn validate-required-fields
>
> >
>
> >
>
> > "Derive errors associated with missing required fields"
>
> >
>
> >
>
> > [state]
>
> >
>
> >
>
> > (postwalk
>
> >
>
> >
>
> > #(if (is-required-field? %) (validate-required-field %) %)
>
> >
>
> >
>
> > state))
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> > (defn derived-state
>
> >
>
> >
>
> > "Used to include derived state for use by components."
>
> >
>
> >
>
> > [state] (-> state
>
> >
>
> >
>
> > ;derive-vertical-required
>
> >
>
> >
>
> > ;end-position-logic
>
> >
>
> >
>
> > ;maint-freq-logic
>
> >
>
> >
>
> > validate-required-fields))
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
> >
>
>
>
> > On Mon, Feb 16, 2015 at 4:17 AM, Leon Grapenthin <[email protected]>
> > wrote:
>
> > If they were changing, you could implement your own reference system, where
> > you have one stateful lookup map that you make globally available via a ref
> > cursor. Instead of the actual changing values, you'd use lookup keys in
> > your rendered cursors. Changes to values in the stateful lookup map would
> > would reflect everywhere they are looked up.
>
> >
>
> >
>
> >
>
> >
>
> >
>
> > On Sunday, February 15, 2015 at 3:54:36 PM UTC+1, Scott Nelson wrote:
>
> >
>
> > > I’m working on an Om application that includes a music browser and player
> > > with a simple artist -> albums -> songs hierarchy. When a user is
> > > viewing an artist section I layout all the artist’s albums and songs
> > > hierarchically and this works great since there is basically a 1-to-1
> > > correspondence between the component hierarchy and the data hierarchy.
>
> >
>
> > >
>
> >
>
> > > Here’s a basic example of what a piece of the application state might
> > > look like under some sort of :current-artist key:
>
> >
>
> > >
>
> >
>
> > > {:name “The Beatles”
>
> >
>
> > > :albums [{:name “Abbey Road”,
>
> >
>
> > > :songs [{:name “Come Together”}
>
> >
>
> > > {:name “Something”}]}
>
> >
>
> > > {:name "Rubber Soul",
>
> >
>
> > > :songs [{:name "Drive My Car"}
>
> >
>
> > > {:name "Norwegian Wood"}]}]}
>
> >
>
> > >
>
> >
>
> > > When a user plays one of these songs I build an ordered playlist of all
> > > the artist’s songs. There is a player component that plays the playlist
> > > (even if the user navigates away from the current artist section) and
> > > displays the currently playing artist, album and song name. To achieve
> > > this I ended up duplicating a bunch of artist/album/song data elsewhere
> > > in the application state.
>
> >
>
> > >
>
> >
>
> > > Here’s what the :playlist path of the application state might look like
> > > after a user clicked the play button next to “Drive My Car”:
>
> >
>
> > >
>
> >
>
> > > {:play-index 2
>
> >
>
> > > :songs [{:name “Come Together”
>
> >
>
> > > :album {:name “Abbey Road”
>
> >
>
> > > :artist {:name "The Beatles"}}}
>
> >
>
> > > {:name “Something”
>
> >
>
> > > :album {:name “Abbey Road”
>
> >
>
> > > :artist {:name "The Beatles"}}}
>
> >
>
> > > {:name “Drive My Car”
>
> >
>
> > > :album {:name “Rubber Soul”
>
> >
>
> > > :artist {:name "The Beatles"}}}
>
> >
>
> > > {:name “Norwegian Wood”
>
> >
>
> > > :album {:name "Rubber Soul"
>
> >
>
> > > :artist {:name "The Beatles"}}}]}
>
> >
>
> > >
>
> >
>
> > > I think this sort of denormalization works fine in my case since the
> > > artist/album/song data is not changing but I'm wondering if there is a
> > > better way to accomplish this. If the data were changing then I would
> > > imagine it could become a challenge to keep the playlist data in sync.
>
> >
>
> >
>
> >
>
> > --
>
> >
>
> > Note that posts from new members are moderated - please be patient with
> > your first post.
>
> >
>
> > ---
>
> >
>
> > You received this message because you are subscribed to the Google Groups
> > "ClojureScript" group.
>
> >
>
> > To unsubscribe from this group and stop receiving emails from it, send an
> > email to [email protected].
>
> >
>
> > To post to this group, send email to [email protected].
>
>
>
> >
>
> > Visit this group at http://groups.google.com/group/clojurescript.
>
> >
>
> >
>
> >
>
> >
>
> >
>
> > --
>
> >
>
> >
>
> > Oliver George
>
> > Director, Condense
>
> > 0428 740 978
>
>
>
> --
>
> Note that posts from new members are moderated - please be patient with your
> first post.
>
> ---
>
> You received this message because you are subscribed to the Google Groups
> "ClojureScript" group.
>
> To unsubscribe from this group and stop receiving emails from it, send an
> email to [email protected].
>
> To post to this group, send email to [email protected].
>
> Visit this group at http://groups.google.com/group/clojurescript.
>
>
>
>
>
> --
>
>
> Oliver George
> Director, Condense
> 0428 740 978
--
Note that posts from new members are moderated - please be patient with your
first post.
---
You received this message because you are subscribed to the Google Groups
"ClojureScript" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/clojurescript.