Thanks for looking. Yeah, I have not had much time to work on it/review my work…there are a number of issues remaining. The API naming is a bit off in places, might want to split things more…
🙏 Hi folks! Two and a half quick questions: 1. Are you learning Fulcro? How/where do you find learning materials? Do you know about the https://fulcro-community.github.io/ site and the https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/index.html? We are eager for any feedback on improving the content and its visibility!!! 2. Are you teaching Fulcro to somebody? How does the Minimalist Fulcro Tutorial work for you? Any input? Thank you!!!
I think something that could useful would be a sample app in re-frame vs fulcro showing the differences and benefits of fulcro. Re-frame is the most popular cljs lib for spas so many people coming to fulcro are already familiar with it but have a hard time with the mind shift.
More code examples would be good, especially because I cannot find a terribly great number nor diversity of open-source, real-world Fulcro applications. Tony Kay's videos and open-source code examples were very helpful — they put the principles into practice (to an extent) — but they do not thoroughly demonstrate a wide diversity of apps and situations. Ideally, I would like to analyse and refer to comprehensive code examples that have annotations/documentation that explain what everything does, how things could be done differently, and why things were done a certain way over another. Seeing how the individual parts compose into a whole helps consolidate understanding and emphasises best practices. That said, this approach is probably only useful for people who have already gained some familiarity with the basic concepts of Fulcro, and are then looking to apply and tie together the knowledge. It is also quite expensive to create these good examples
I would personally benefit a lot from watching a live/recorded coding session of a minimal app that touches on the main things (e.g. pulling data from db, authentication, creating views, etc). I’ve struggled most from reading the documentation and mapping that knowledge to my app. Or perhaps some coding exercises where pieces of code from a working app are left out and you have to fill those in to get the app working again (with comments for hints).
@jorda0mega Have you seen Tony Kay's https://www.youtube.com/watch?v=wEjNWUMCX78&list=PLVi9lDx-4C_T7jkihlQflyqGqU4xVtsfi?
Another perspective: I was busy with backend stuff for the last couple months and just returned to fulcro. The minimalist tutorial is a really nice refresher to get up to speed again. Great job, thank you!
Thank you for the kind words! It helps to keep motivated :)
@luis.tn yeah most of them but I got the impression they were more isolated videos about a specific topic rather than a follow through to a finished app. Maybe I should re-visit them. It’s probable I didn’t grok them at the time
@holyjak love the work you have done to make fulcro more accessible to us newbies. Very much appreciated from my end!
Hi! @holyjak thanks you for questions. Personally I find tony.kay's work is amazing, but the language (mostly style of narration) is a bit hard form me. Now I try to find easier way to explain (mostly for myself) Fulcro's concepts. One of first thing that I change when I open Fulcro Dev Guide is font 🙂 Would be awesome to have ability to change it on some sans- serif. 🙂 I still didn't dive into Fulcro Community docs, because mostly using FDG and source code, but for now the most confused part of Fulcro documentation is Dynamic Router, Initial State and Initial state for Dynamic Routers. Now I try to systematise deferent "types" of components and relations in Fulcro. I never can say which type of ident I should use, where store the data, etc. So, more clear info about structure of front-end components would be great. And thank you a lot for your work!
Could you elaborate on why you struggle with what kind of ident to use (and what options do you consider here) and your uncertainty about where to store data? Concrete examples would be best.
Sure, I'm on it right now. I'll present it on channel, as soon as I finish and send you a link. Don't want to spoil.
IMHO initial state is simple - include it always, use the template form, and simply include the children 's state. Don't do anything fancy with it.
hey @holyjak, thanks for the work you're doing on documentation. I'm learning fulcro. I was aware of the Fulcro Developer Guide and the various collection of videos on youtube, but not of the fulcro community website or the minimalist tutorial. Nice! Since writing code as I go along is my preferred method of studying new tech, the Fulcro Developer Guide is the format I prefer. There's an improvement I can think of: when you go through the initial example, a git repo with tags for each functional state would be very nice in case one introduces some errors and wants to continue from a known check point.
This is exactly what I was looking for. Official fulcro guide is overwhelming.
Thank you! Any ideas how to make the Community site and the Minim. Tutorial easier to find / more visible? (William - I very much understand you but https://fulcro-community.github.io/guides/tutorial-minimalist-fulcro/index.html#_a_word_of_warning still applies 🙂. I am (slowly) working on a follow-up to the tutorial, that would be a hands-on series of exercises. Still, I see the tutorial as a necessary first step. Of course, everybody is unique and YMMV...)
I think there should be a section on top of the fulcro book saying something like "Additional resources" where community site would be listed.
I agree. That way, you can reliably ensure that it gets in front of the target audience. Anyone who is looking to learn Fulcro will stumble upon the book, and seeing a shorter tutorial will be especially appealing to those who get initially intimidated by the sheer length of the book.
Thank you! I will have a look at what I can do about it.
After some more playing around with the 'raw' facilities, I've encountered a shortcoming of the way state updates are sent to components.
Currently, each render listener is called sequentially using doseq
. Let's say I'm using multiple use-root
/`use-component` hooks at the same time, each registering their own render listeners. The problem is that after each render listener is called, the local state of a component is updated which causes React to re-render that component immediately; each render happens one-at-a-time (one per hook), as can be seen using the React Devtools profiler.
This is probably not much of a problem if raw components occupy distinct and isolated branches of the UI tree, but it has caused confusing problems for me when I have had one of these components be a descendant of another in the UI tree.
In my case, I have implemented some dynamic routing where each route has its own raw child component. When I update the state by typing in an input component, the root component's state is updated before the children's states. React re-renders the root immediately but also re-renders all of its children, leading to inconsistent state across the UI (the children still have outdated state). Interestingly, for me this meant the input is given the props from the prior state, reversing the changes to the input's own local state, triggering onChange
, and firing off transact!
again.
I've worked around this by wrapping the render!
function's aforementioned doseq
with js/ReactDOM.unstable_batchedUpdates
(which is https://react-redux.js.org/api/batch) so that all changes to component state happen atomically without renders in-between. The drawback, of course, is that this couples the core rendering to ReactDOM (or React Native). There probably needs to be some more modularity here, but I have no good solution right now.
Thank you for your thoughts.
Using a global var is an interesting idea. The issue I see is that the dom
namespace may not required at all. For instance, I have been using the Helix library to create React components.
I think something like a batch-updates
parameter could be doable if it were abstracted away using wrappers around fulcro-app
for DOM/Native (like how fulcro-rad-app
sets reasonable defaults). Perhaps something like fulcro-react-app
could be used with an optional react-native?
parameter, for example.
In order to work optimally with current and future versions of React, I do believe that compromises like this are necessary.
As for your final point, I largely agree that Fulcro has the right model for building UI. However, I feel like I have been swimming against the current whilst trying to implement a more dynamic UI tree where different pieces get swapped out for others. Specifically, my problem involved displaying a table of rows that simply display the data in each row. When the user clicks on a row, I want each cell to be replaced by input components so the user can edit the data. This 'row editor' component uses different queries and components compared to the 'row viewer', and I do not like the idea of many rows querying for things they do not need.
I tried using set-query!
but that appears to only work on an entire class rather than individual instances. Routers also looked unsuitable because they must be a singleton. While there may be a more standard solution that I do not know about (I only started learning Fulcro two weeks ago), the only good solution I found is to use the raw components.
Another reason I moved away from using defsc
components is that when using use-hooks?
with the key frame renderers, they would always re-render with every change to the state because they do not implement shouldComponentUpdate
and React.memo is not used. I've found this can hurt performance significantly.
Furthermore, I would speculate that the raw API would make compatibility with React's concurrent mode easier, though I have not done any experimentation here.
Yeah using set-query to change a particular instance query was one of the first things I tried in fulcro as well 🙂
@luis.tn For the row editor, I'd consider combining both functions in the row component with (if editing? xxx yyy) in the body and, when load!-ing the data, omitting (:without) the editor's data and only loading them on demand, when you are switching into the edit mode. You could perhaps also add custom Metadata to the query and make some general query pre-processing to omit based on the meta...
Thank you @holyjak for the suggestion. I actually have not bothered to configure a proper backend as I do not intend on using a server, so I've skipped using load! altogether. However, this is certainly something I should explore when I get the time! In fact, I think I was doing something similar to this before using merge-component. I was thinking for the row query:
[{::editor (comp/get-query Editor)}
{::viewer (comp/get-query Viewer)}]
I have split it up like this as the sub-queries may include link queries and I am assuming the ident-optimised renderer would not refresh the parent row component if the result of a link query of a child changes.@luis.tn Since you mentioned using `set-query!` and that "appears to only work on an entire class rather than individual instances", I wonder if query ids can be of help to you: https://book.fulcrologic.com/#_query_ids
@luis.tn If I understand you well, you do this to improve the performance. Have you seen it being a problem in practice? Or do you just suppose that it would be a problem? (BTW, notice the ident-optim. renderer is not the default anymore; the multiple-root one is, which derives from keyframe2, i.e. not ident optimized). Could you be so kind and have a look at https://gist.github.com/holyjak/8f5a5ad4924d51ff5500c6c5b6157246#a-row-with-edit-vs-show-mode-and-sub-components-loading-on-demand and tell me how to make it clearer? 🙏
@hk9861 Would that solution involve dynamically creating factories within a component? (There are not a fixed number of rows; they are generated from the data.)
@holyjak You bring up a very valuable point — I might just be overcomplicating things and doing unnecessary optimisations in the name of perfectionism. After all, the rows would not be querying for anything that would frequently update. If I were to attempt the problem again, I feel like it would be possible.
I suppose the real problem I am concerned about is a more theoretical one that would involve a much more complex UI.
Example: In Roam Research’s app, each text block can contain any number of arbitrary components as defined by the user (eg checkbox, word counter, block reference/embed). These components may want to pull data from the local block as well as other sources. It does not seem practical to have the block component query for every possible sub-component that can exist within the block, as there can be many. The user is even able to create their own custom components using ClojureScript. Perhaps a floating root might be useful for this situation, which leads me onto this:
I switched to the ident-optimised renderer because, https://clojurians.slack.com/archives/C68M60S4F/p1618755812145200?thread_ts=1618696584.142500&cid=C68M60S4F, components that use hooks are over-rendered under the default renderer, which has a very noticeable performance impact such as when typing into an input. Thus, if I wanted anything like floating roots, I would need to use the raw components.
And why might I want hooks? They make certain things more convenient, e.g. useMemo makes it easy to memoize a computation. When used with Fulcro, they make it easier to subscribe to data — especially derived data — using a single function call without having to split code between class lifecycle methods. Tony Kay had a few videos on this topic of derived state.
For example, I made a hook to read a table of normalised entities in the database (from a link query, say, [:person/id '_]
) and whenever the table changes, the new entities are merged into the table component which includes a query join like {::rows (comp/get-query PersonRow)}
. This is because I felt it was a bad solution to :append a newly created entity to every component that depends on all people.
Although, that is a bad example since I assume the correct solution is to instead maintain a centralised :all-people vector at the root and use {[:all-people '_] (comp/get-query PersonRow)}
without having to convert the table to a vector. I suppose it is more manageable/reasonable to add and remove idents from this :all-people list in the appropriate mutation handlers. Originally, I was irrationally avoiding this solution as I did not like having to manually keep things up to date and in sync. However, now I am seeing the advantage of a separate list — e.g. I do not need every person loaded into the database at :person/id at once, yet I can still have an index of :all-people.
Another reason for hooks is that I feel they will work better with React’s Concurrent Mode, and I think it is worth being compatible with this for the sake of a good user experience. I have no idea how concurrent mode would work with Fulcro, but I expect there to be some challenges because React likes to manage its own state. Perhaps a raw Fulcro app will have a better chance of compatibility as Fulcro is less tightly coupled to the rendering and I feel like I have more control. Concurrent mode is a big reason why I have started to look at approaches that lean into React like using the Helix library.
So that was me trying to explain the challenges I'm seeing with Fulcro. My big questions are concerned with how well Fulcro can model a highly dynamic and unpredictable UI structure, like I described with the Roam Research example — pushing it to the limits. If you read through this, I appreciate your time and I also appreciate everyone who is helping me learn how to use Fulcro better.
As for the gist, I'd say it is clear enough for me to get an idea of the general ideas. Seeing code snippets helps me understand things. Though, I would have to do some more experimentation with code to understand the details of the problem better.
thanks for all your thoughts and exploration @luis.tn, I share most of your feelings around that, I’ve recently started playing with Fulcro Raw as well, and I love having a pure hooks interface to the fulcro state management system
Thanks a lot for the thorough explanation!
I’ve added some basic logic to support doing the batched render in SHA ae128d71d8bcdd36ffdbf43f26e35e5508ce8b6b @luis.tn. I also patched the other bits you noted. On dynamic queries: It is impossible (without additional help) to set the query of a component instance because there can be more than one on the screen at a time, even with the same ident, because such components are path dependent, and dynamic queries should not be tied to component-local state. Dynamic queries actually get normalized into the app db, and are therefore serializable with the entire app state, making a render (even with dyn queries) truly state dependent. If you look at the book there is metadata that can be added to a factory that can be used to alter the query ID (which defaults to the FQ classname). This allows you to use a factory to set the query instead of the component, which in turn lets you target a set query to a particular call-site (you just make extra factories). Now, of course, this doesn’t scale to the example where you want a different query for every row of a table (though technically you could do that as well…but it would be overkill). The “disconnected root” mechanism really is best for scenarios where you want to spring a component into existence without having to pre-compose state/query.
Another perfectly valid alternative for tables that have form inputs is to make the row-level components leverage component-local state for editing, and send a single transaction to the parent of the table to “commit” the edit. The top-level table component just tracks which row is editable, and the rows themselves do not need joins to “stateful inputs”. There are, of course, exceptions. Using the new raw support to spring those into existence for just the selected row is perfectly tractable, and should perform quite well. Especially with the latest optimizations that don’t re-send props unless they actually change.
Thank you for following up with the explanation and adding those patches!
Also, I notice that when using the updated hooks, the React component will have to account for the props being nil on the initial render until add-component!
merges the initial state, which causes a subsequent render.
I'll do some more experimentation...
Yes, I didn’t love the nil first return, but I have not had time to work out what the exact initial state should be. use-state can accept a function for construction, so I should be able to get it to work better. Feel free to give it a shot if you want to send a PR 😄
I appear to have got something to work. My first version involves changing the behaviour of add-component!
such that it does not call receive-props
nor add-render-listener!
immediately, but instead returns both the initial props and the function that registers the render listener. This means the consumer of add-component!
must manually call the returned registration function (which, in React's case, will be done inside of a useEffect hook).
An alternative way that conserves the functionality of add-component!
is to have the use-component
hook steal and adapt the implementation of add-component!
rather than using that existing function — with perhaps common functions between them. Personally, I like this approach more. I'll start working on that.
@tony.kay
Okay, I've made an attempt at improving a few parts of the code. The changes can be reviewed here:
https://github.com/fulcrologic/fulcro/compare/feature/fulcro-3.5...LuisThiamNye:feature/fulcro-3.5
I have not encountered any issues with this so far. Please let me know of any problems with these changes or other feedback.
Here's a summary of the changes:
• Changes to hooks:
◦ The component state is initialised on the first mount of the component, then current-props are initialised and ready to use on the first render
◦ An effect hook manages the addition/removal of the render listener
◦ use-component:
▪︎ Randomly generates a UUID to use for the render listener so that multiple components can share the same ident
▪︎ Uses the :ident
option if provided
• Changes to raw.application
◦ In render!
— use :batch-notifications
(not :batch-renders
) to match the key provided to the application.
◦ In add-component!
and add-root!
— Refactor out parts into helper functions which are also used by the hooks. (One of these functions I put in raw/components)
◦ add-component!
uses the :ident
option if provided, and initialize?
is now true by default
• Replaced references to components/after-render with raw.components/after-render
◦ This fixes some 'undeclared var' errors being thrown after I reloading the raw.components namespace. (I do not fully understand the problem — and this is my first encounter with dynamic vars).
◦ The *after-render*
var in the original components namespace does not seem to be used anywhere, from what I can understand from the code. If so, there may be a larger problem of this var being completely ignored. :man-shrugging:
that large amount of whitespace change is undesirable, as it makes it a bit harder to read. Consider adding this alias to your git:
[alias]
addnw=!sh -c 'git diff -U0 -w --no-color "$@" | git apply --cached --ignore-whitespace --unidiff-zero -'
in ~/.gitconfig, then you can do git addnw .
to add all non-whitespace changes to a commit (and then once committed, discard the ws changes)some good refactoring. I’m going to convert this to a PR so I can commit on it inline
or perhaps you should, so that it doesn’t end up in a state you can’t push to?
Thank you for the tip! I pushed a new version without the whitespace/alignment modification, but there is some whitespace weirdness in some places. If you are comfortable with these changes, I can create a PR.
Please do. That way I can comment by line
It's been submitted
thanks