omcljs / om

ClojureScript interface to Facebook's React
6.66k stars 363 forks source link

One query, multiple components #823

Open julienfantin opened 7 years ago

julienfantin commented 7 years ago

We have a common use-case where a parent needs to render multiple children for the same query, which is not supported:

(defui ^:once ChildOne
  static om/Ident
  (ident [_ {:keys [db/id]}]
    [:db/id id])
  static om/IQuery
  (query [_]
    [:db/id
     :doc/relevant-to-child-one])
  Object
  (render [this]
    (dom/h2 nil
      (str "Child One " (-> this om/props :doc/relevant-to-child-one)))))

(def child-one (om/factory ChildOne))

(defui ^:once ChildTwo
  static om/Ident
  (ident [_ {:keys [db/id]}]
    [:db/id id])
  static om/IQuery
  (query [_]
    [:db/id
     :doc/relevant-to-child-two])
  Object
  (render [this]
    (dom/h2 nil
      (str "Child Two " (-> this om/props :doc/relevant-to-child-two)))))

(def child-two (om/factory ChildTwo))

(defui ^:once Parent
  static om/IQuery
  (query [_]
    [;; What about ChildTwo?? (om/get-query ChildTwo)
     ;; Queries will overwrite each other
     {:app/doc (om/get-query ChildOne)}
     ;; {:app/doc (om/omget-query ChildTwo)}
     ])
  Object
  (render [this]
    (let [props          (om/props this)
          children-props (:app/doc props)]
      (dom/div nil
        (child-one children-props)
        (child-two children-props)))))

(def state
  {:app/doc
   {:db/id                     1
    :doc/relevant-to-child-one "Child One Info"
    :doc/relevant-to-child-two "Child Two Info"}})

(def parser
  (om/parser
   {:read
    (fn [{:keys [state query] :as env} k _]
      (let [st @state]
        {:value (om/db->tree query (get st k) st)}))})

(def reconciler
  (om/reconciler {:state  state :parser parser}))

A variadic get-query with multiple paths in the props' meta may be a way to allow this?

In the above example the API may be as simple as:

(defui ^:once Parent
  static om/IQuery
  (query [_]
    [{:app/doc (om/get-query ChildOne ChildTwo)}])
  ...)
anmonteiro commented 7 years ago

Why can't you use a union query?

julienfantin commented 7 years ago

Because it'd require duplicating the parser's read method for that key.

den1k commented 7 years ago

@anmonteiro this helper would do {:app/doc (vec (into #{} (mapcat om/get-query) [ChildOne ChildTwo]))} but the actual problem is loss of metadata. There is no support for multiple component tags on one query, so we often end up with the (str "No queries exist for component path " cp " or data path " path') error. We could recurse with the parser and specify read-keys like :app.derived/child-one and :app.derived/child-two but that should not be necessary.

anmonteiro commented 7 years ago

Sorry, but this is the exact use case for which union queries were devised.

You might be missing something about how they work too, as you don't need separate read methods for union queries. Even if you did, you can factor that into a single common function that knows how to treat both use cases. Om Next is not interested in providing sugar which you can achieve with other abstractions.

I'm happy to discuss this more in depth in the #om Slack channel, but I think this is a non-issue.

den1k commented 7 years ago

I think we have fundamentally different use case. For example, there's nothing like a type we can use to build idents. It's similar data with the same read key that's passed down to more than one component. Happy to discuss in #om.

wilkerlucio commented 7 years ago

@den1k I understand your use case, I have discussed that with other people at the #om channel. The current best solution that I know for that is using some sort of "placeholder nodes", this is something you can implement on your server. The idea is to have a special keyword namespace, and when you use that namespace you just keep at the same read level as before, so you can do, for example:

[{:ph/foo [{:baz (om/get-query Foo)}]}
 {:ph/bar [{:baz (om/get-query Bar)}]}]

So, in this case ph would be the special keyword namespace, on your server implementation you can check for this, and recursively call the parser again, this way you can have multiple versions of the same key with as many names as you want.

den1k commented 7 years ago

@wilkerlucio Thanks! We know we can work around this through the parser. The point is, I don't think we should have to.

swannodette commented 7 years ago

I was able to discuss this in person with @den1k and @julienfantin. This one will need a bit of hammock time but I'm interested in a seeing a good solution in the near future.

den1k commented 7 years ago

For a practical example, imagine a Markdown editor with live-preview. Two components side by side that need the same state and idents, and should update whenever the document changes.

Peeja commented 7 years ago

I've been toying in my head with the idea of some kind of aliasing in the query, such as:

[{[:app/doc :> :doc-for-child-one] (om/get-query ChildOne)}
 {[:app/doc :> :doc-for-child-two] (om/get-query ChildTwo)}]

the idea being that [:app/doc :> :doc-for-child-one] is an alias form which reads :app/doc in the parser but returns its value under the key :doc-for-child-one. This gives each child its own node in the query tree, but still lets us read their queries on the same key.

I'm not sure how feasible that would be to implement. Syntax-wise, though, a three-element vector in key position is conveniently undefined in the current query syntax.