The trouble with list components in Fulcro
Imagine you have a list of things to display in your UI. You naturally want to represent them with a list component, such as a TodoList
. But that is not the way we do it in Fulcro, which beginners find confusing (I did). Here I explain why and what are the alternatives.
Still new to Fulcro? Make sure to check out my Minimalist Full-Stack Fulcro Tutorial!
The thing is that in Fulcro a component (normally) represents a "data entity" and a list is no entity, it is just a grouping of entities. It is clear on the example of children: a Parent
component has some number of Child
sub-components; it does not have a "list of children". This is how it looks in code:
(defsc Child [_ {:child/keys [name age]}]
{:ident :child/id
:query [:child/name :child/age]}
(li name " is " age))
(defsc Parent [_ {:parent/keys [name children]}]
{:ident :parent/id
:query [:parent/name
{:parent/children (comp/get-query Child)}]} ; (1)
(div "Children of " name ":" (ul (map (comp/factory Child) children))))
1 | There is no "List" component; the parent queries for its children property and gets the list. |
See the full code here.
This is how the data looks in the Fulcro client DB:
{:parent/id {1 #:parent{:id 1, :name "Darth Vader",
:children [[:child/:id 22],...]}}
:child/id {22 #:child{:id 22, :name "Leia", :age 21}, ...}
...}
I still want my List!
Let’s suppose that you actually want an explicit list component instead of displaying the children directly in the parent component. A good reason could be that the parent component is already big and the way you want to display the children is non-trivial. What to do? You have three options, in order of simplicity:
Create the List as a UI-only, stateless component
Ask Pathom to insert an extra level of structure for the list component
Create the List as a stateful component and add the necessary "edge" to the data tree manually
1. Create the List as a UI-only, stateless component
Create the list as a UI-only, stateless component without an ident, query, or client-DB-provided props of its own. The original parent still queries for the children but passes them to the list component for rendering.
See Divergent UI and Entities - A UI-only component for details.
; ...
(defsc ChildrenList [this _]
{}
(div "ChildrenList is:"
;; Notice we copy the `:keyfn`-provided unique key to the div
;; of each of the list elements, as React requires
(map #(div {:key (.-key %)} %) ; (1)
(comp/children this)))) ; (2)
(def ui-child-list (comp/factory ChildrenList))
(defsc Parent [_ {:parent/keys [name children]}]
{:ident :parent/id
:query [:parent/id :parent/name
{:parent/children (comp/get-query Child)}]}
(div "I am the terrible" name "!"
(ui-child-list {} (map ui-child children)))) ; (3)
1 | Copy the unique key (provided by Child’s factory’s :keyfn ) to make React happy |
2 | Access the child React elements (= already react-ified components) via comp/children |
3 | Only the Parent can see the children data property and instantiates the individual Child components, passing them their props; it then gives the instantiated elements to ChildrenList for rendering. The list here works as kind of a "template" with "slots" for the elements to be rendered |
See the full code of a stateless list here.
This is the "purest" way of passing the individual elements from Parent to ChildrenList, i.e. letting Parent to create React elements for them and passing these as React children to the list component.
Alternatively you could just pass the children prop down to the list component either inside its props or, to be 100% safe, as a computed prop (see Parent-computed Data in the Book for details). Both should work equally fine as long as you do not use the optional ident-optimized renderer. It would look like this:
; ...
(defsc ChildrenList [_ props]
{}
(div "ChildrenList is:"
(map #(div {:key (:child/id %)} (ui-child %))
(:children props))))
(def ui-child-list (comp/factory ChildrenList))
(defsc Parent [_ {:parent/keys [name children]}]
{:ident :parent/id
:query [:parent/id :parent/name
{:parent/children (comp/get-query Child)}]}
(div "I am the terrible" name "!"
(ui-child-list {:children children})))
2. Ask Pathom to insert an extra level of structure for the list component
If the data you’re using is coming from Pathom then you can ask it - via a Pathom placeholder - to insert an extra layer between the original parent and the children, to fit the list. Thus the list will exist as a stateful component. That is especially useful if you want it to have state of its own, for example to keep track which of the list elements the user has selected.
See Divergent UI and Entities - A Data Entity spread across multiple (sibling) components for details.
; ...
(defsc ChildrenList [_ {:parent/keys [children]}]
{:ident :parent/id ; (1)
:query [:parent/id
{:parent/children (comp/get-query Child)}]} ; (2)
(div "ChildrenList is:"
(map #(div {:key (:child/id %)} (ui-child %)) children)))
(def ui-child-list (comp/factory ChildrenList))
(defsc Parent [_ {:parent/keys [name] :as props}]
{:ident :parent/id
:query [:parent/id :parent/name
{:>/children-list (comp/get-query ChildrenList)}]} ; (3)
(div "I am the terrible" name "!"
(ui-child-list (:>/children-list props)))) ; (4)
1 | Copy the ident of the original parent component |
2 | Copy the relevant part of the original parent component’s query |
3 | Use the Pathom placeholder (by default > ) with an arbitrary name
(here children-list ) and the query of the new list component |
4 | Pass the children-list props down |
3. Create the List as a stateful component and add the necessary "edge" to the data tree manually
If you want the list component to exist as a full-fledged, stateful component and the data isn’t coming from Pathom and thus you cannot leverage the placeholder facility then you can replicate it manually. It means that you need to insert an extra "edge" between the original parent and the list and move the list property from the parent into the list entity.
If the parent and list components are static (i.e. their idents do not depend on props and they exist at application start) then you can use the list’s :initial-state
and its inclusion in the parent’s to establish the edge. In the case of dynamic components you would likely leverage :pre-merge
or a custom mutation. You would also use either pre-merge or a mutation to move the list property from the parent into the list entity.
See Divergent UI and Entities - Inserting a stateful UI component… for details.
; ...
(defsc ChildrenList [_ {:parent/keys [children]}]
{; :ident nil ; (1)
:query [{:parent/children (comp/get-query Child)}]} ; (2)
(div "ChildrenList is:"
(map #(div {:key (:child/id %)} (ui-child %)) children)))
(def ui-child-list (comp/factory ChildrenList))
(defsc Parent [_ {:parent/keys [name] :as props}]
{:ident :parent/id
:query [:parent/id :parent/name
{:artificial/child-list (comp/get-query ChildrenList)}] ; (3)
:pre-merge (fn [{parent :data-tree}] ; (4)
(-> parent
(assoc :artificial/child-list
(select-keys parent [:parent/children]))
(dissoc :parent/children)))}
(div "I am the terrible" name "!"
(ui-child-list (:artificial/child-list props)))) ; (5)
1 | Here the list component has no ident because I see no point in normalizing it
(alternatively, we could give it :ident :parent/id in which case :artificial/child-list
would point to itself; we wouldn’t dissoc :parent/children and do few more changes;
see a fully worked out example here) |
2 | As in the case of Pathom, we copy the relevant part of Parent’s query (though we could have renamed the key in the pre-merge and then would need to adjust the query accordingly) |
3 | The Parent queries for the manually inserted edge, including ChildrenList’s query |
4 | In pre-merge we restructure the data as needed, inserting the extra level of :artificial/child-list .
Merging then continues recursively with the newly inserted property and the ChildrenList query |
5 | The Parent instantiates the ChildrenList component, passing it the artificially created property / edge. |
See the full code of a stateful list here.
Summary
A list of entities is not an entity itself - it is just a prop on a parent entity. Thus you cannot create a dedicated component to encapsulate the rendering of the list out of the box. But there are few ways to enable you having a dedicated list component, either stateless or stateful, depending in your needs.