Fork me on GitHub

I start a new SPA, just one panel, so it is totally ok to put everything into app-db, but later the application need more panels, then I switch into "larger application structure", but unfortunately, the new panels need their own app-db, so the path to my data will changed, it means I need to change the subs/events. For events, I need add new path interceptor, and for subs, I need manually change all the subscriptions.


This is painful, and IMO, not the best practice to scale an application.


If we make the app-db a recursive data structure, allow each panel just see its own sub-app-db (a ratom in the whole one), then we need not change code for move panels in the application.


why don’t just register each major part of application (can consist of several panels) in db under unique path and have all subs/controllers working of this path in db?


I have something like this in a quite large app that I’m working on as db description for each major part of app

(def metadata-db-path [::blueprints])
(def build-db-path (conj metadata-db-path ::build))
(def nodes-info-db-path (conj metadata-db-path ::node))
(def layers-db-path (conj metadata-db-path ::layers))
(def persistent-query-keys-db-path (conj metadata-db-path ::persistent-query-keys))

(def db-structure
  (-> {}
      (assoc-in persistent-query-keys-db-path #{:layer :side-menu})
      (assoc-in build-db-path build-model/build-metadata)
      (assoc-in nodes-info-db-path nodes-model/db-structure)
      (assoc-in layers-db-path physical-model/layers-metadata)))


and then app.db before init gets a lot of dedicated sub-spaces in it

(def default-db


Thanks @kishanov, this is certainly a good structural way to manage application.


However, by talking application scale-up and scale-down, I mean that the freedom of use the whole panel as a component, without knowing if it is a "big" or "small" application, and can even switch between it.


like a stand-alone component or container in which other component can be nested?


For example, I found the todomvc is a good feature in my application, and the best approach is just import it as a whole into my app, without change a single line of code, just do not call its core/main of course.


oh, I see. we solved this problem for us but I’m pretty sure that it doesn’t haven anything to do with best practices 🙂


here is the approach:


for the whole application we maintain a sitemap which is a clojure map which maps routes to what should happen to these routes (including which UI component should render, which API calls should be done, etc.)


for example, here is a definition of a sub-application which which handles CRUD for one of our REST resources:


after this “application” was defined, it can be registered in sitemap. registaration means parse all route definitins + metadata and register it in app db


after application’s sitemap has been registered there is an “on-navigate” function that handles routes and determines which information should be taken from sitemap (for rendering, fetching data, etc.). Thanks to namespaced keys there are no name collisions in routes definition


and when the application is mounted and navigates to particular URL it just parses this metadata structure and determines what to render, which controllers to initiate, etc.


important thing that when you “mount” sub-application in sitemap structure you provide a key to which it should be mounted. if it mounted to a deeply nested structure you technically build a tree-like structure that represents the whole app and can traverse from current leaf of the tree upwards to collect information about how it should be mounted and which actions should be executed/which higher-order components should wrap your component


Allow some time to digest your suggestion. Now I did some experiment, that by allow sub-app (ie a panel, like todomvc) to create app-db by itself, (def app-db (re-frame/make-app-db)), then the other context (larger app) which just mount it in its app-db {:todomvc-db (todomvc.db/app-db)}, and its done.


we did something similar to this before, but I didn’t like the approach to put anything except metadata into app-db (views should be separated)


this is how my sitmeap/core.cljs looks like:

(def sitemap-def
  (model/register-apps [auth/application-map

(def ui-metadata (get-in sitemap-def model/ui-metadata-db-path))
(def db-structure (model/prepare-db-structure sitemap-def))


I prefer to store in db only things related to state, but my :on-navigate handler should also understand which root component to mount/which controllers to kick in


because of this we decided to have a single maps route-key to everything required for this route, literally views, containers, controllers, etc. but we only store in db metadata for the app, while this big ugly sitemap exists as a var available from everywhere


Does that means the sub-apps still need aware it is in a bigger context, and follow the convention?


I can not quite understand "put anything except metadata into app-db (views should be separated)"


ok, here is the single entry to “big-application-sitemap”

    {::sm/route-def "/resources/ip-pools/:ip-pool-id"
     ::sm/views     {::sm/container   (fn [view-component]
                                        [container-views/fetch-get-container [[::routes/api-details 5000]]
                     ::sm/active-view (fn []
                                         [[rest-resources-views/edit-resource-btn ::routes/ui-edit]
                                          [rest-resources-views/delete-resource-btn ::routes/api-delete ::routes/ui-list]]
     ::sm/metadata  {::sm/title-fn (partial rest-resources-dto/resource-title-fn ::routes/api-details)}}


information that should go to app-db is routes-def and metadat. information about views is “what should be rendered when you get there and how it should be rendered`. I prefer not to store it in app-db (cause these are Reagent components with their own state an everything)


From my perspective, if the state in todomvc is managed by itself, then the larger context is the one knows a larger context exists.


So no additional "states" in the whole map.


in your case “todomvc” app should probably claim some unique path in app-db and work with it. in this case when todomvc comopnet is mounted it will init it’s own state and will handle it


Yes. I think it is a good thing to keep local state (local for the todomvc app) should be contained by itself.


Please notice the larger context just contains sub-app-db, so it does not introduce view depencies.


I’ve written a datatable component that I consider a poor man’s verison of a reusable component (, they way how it works with reagent - it accepts an id of a table which helps to create a unique path in app-db to store metadata info about it and work with the rest of re-frame via subscriptions


again, don’t think it’s the best practice, just something that works for us in a pretty sizeable app (~100 screens)


when component is registered it utilizes react’s lifecycle method :component-did-mount to register itself in app-db

kishanov04:04:37 - after it component is independent from the rest of the state and can have it’s own state that it needs


awesome @kishanov , I will check it out. I forked the re-frame codebase to try out my idea, on . It is a naive implementation now, one macro re-frame.core/make-app-db written to provide subscribe, reg-sub from user defined app-db, while the default app-db is still there.


@kishanov your datatable looks awesome!


A component register itself with app-db is a nice trick, but it is a local state store to global state pattern, if the state is just about the component itself, it is fine, but I am looking for a way let outside world (the owner of the component) a method to manipulate and query the state, so this actually are two different problem.


thanks @mikethompson, I will give soda-ash a shot. semantic-ui looks nice!