re-frame

Nim Sadeh 2023-08-05T20:19:20.833279Z

I am a noob to ClojureScript, though not to SPA development, and I have some questions about whether I should use re-frame. First of all it is nothing against the framework, it looks excellent and well-documented and I appreciate all the work that went into making it. I have some experience building large, complex SPAs. Everything that re-frame makes you do are things I learned to do the hard way and I would 100% do all of these things anyway even if the framework didn't force me into it. I read the re-frame docs (which are great and clear) and I am just... overwhelmed. There's so much: now I need to know what an effect vs a co-effect is and how to distinguish that from an interceptor, when to register which, etc. re-frame also has magic going on, where you call a function with a vector and stuff happens that you didn't expect (took me a while to figure out how to print the app state to repl, e.g.), and magic like that incurs costs as well. The question in my mind now is as follows: CSP/core.async is pretty simple. The way re-frame forces you to write SPA is how I was brought up building SPA anyway. Shouldn't I avoid the mental cost of the framework and its jargon, the complexity in the magic, and the additional dependency and just use channels and ratoms? Again this framework is great, I just as a ClojureScript noob seeing many pros using want to know if I am about to make a huge mistake 🙂

valtteri 2023-08-06T07:54:07.628759Z

I had same thoughts a few years back though I had already learned and used re-frame. I started another project with plain Reagent and it felt good until the app grew above 5 namespaces… then I started longing for re-frame and regretted I didn’t start with it that time. 😉 I tried to be methodological but I still ended up with weird problems that re-frame simply avoids. What I learned is that the way re-frame encourages (or forces) you to do things makes it scalable and causes less mental overhead in the long run.

Nim Sadeh 2023-08-06T13:55:15.954239Z

> With core.async there's no global state and everything that's going on is not inherently observable and instrumentable. the global state thing is totally true - however, as I stated, it's one of the common practice things that I do anyway, so re-frame forcing me to do it doesn't change anything. Would you mind expanding a little about "observable and instrumentable"? One of the pieces of complexity that re-frame does introduce is that stuff is held in variables defined by the framework, as opposed to everything being held in variables I define myself. How is that more observable? I learned ClojureScript because of the cool tooling and observability stuff you have access to with the REPL so this is important to me @danielmartincraig /007 I am currently looking at https://github.com/armincerf/kalm-mobile/tree/master as an example but I am actually finding it quite difficult to grok, for example, every time I see an interceptor I have to sketch out what interceptors do (as opposed to a fairly simple function on variables that I'd be able to read easily)

2023-08-06T14:58:55.400179Z

That example looks pretty complex

2023-08-06T15:00:47.643289Z

I didn’t use any interceptors in this project: https://github.com/danielmartincraig/car-designer

2023-08-06T15:01:43.233779Z

Maybe worth a look, as an example. Particularly if you’re interested in using re-com

Nim Sadeh 2023-08-06T15:24:29.085459Z

Thank you! I'll take a look

Nim Sadeh 2023-08-06T19:28:05.357649Z

Also a question: are you not supposed to use local state (e.g., useState) in re-frame at all? Even for basic small things like the height of a resizable block?

2023-08-06T19:33:49.761589Z

To be clear, this is something that I don’t have a lot of experience with

2023-08-06T19:34:26.213549Z

recently I’ve been hearing that Uix with re-frame is a good choice if you want to use react hooks

2023-08-06T19:35:13.723889Z

https://github.com/pitch-io/uix

2023-08-06T19:35:32.888029Z

https://github.com/pitch-io/uix-starter

liebs 2023-08-06T21:13:39.522859Z

FWIW, I was also overwhelmed by re-frame in the beginning. I'm sure I don't have as much experience as you either. I would say I've become comfortable with it by building stuff. I agree that the jargon is tough to swallow, but behind that jargon is a lot of very relieving simplicity. This, for the record, is about as complex as I've gotten: https://github.com/bhlieberman/rf-city/tree/master but it does show, for instance, a component with local state. Also, for inspecting your app-db, I recommend using re-frame-10x.

hifumi123 2023-08-06T21:47:01.607749Z

@nnnsadeh I use Helix with refx, which is re-frame without reagent dependency and designed to work with Helix. The overall principle is the same as ratom vs app-db. That is to say, use-state is good for component-local state. But the moment you find yourself needing to share state across components or use something like react context, then put that data in the app-db and use the use-sub hook to get access to the data

hifumi123 2023-08-06T21:47:24.357799Z

You can set up a helix project with refx by running npx create-helix-app

p-himik 2023-08-06T23:20:22.145249Z

> Would you mind expanding a little about "observable and instrumentable"? Everything it stored in the re-frame.db/app-db ratom that you can deref or watch. There are tools like re-frame-10x and re-frisk that show you a lot more than you can grasp by just looking at the code. By "instrumentable" I mean that you can inject global interceptors and do anything anywhere. E.g. I use them to trigger some workflows when a specific piece of data changes - regardless of how the change has made it into the app-db. There's also add-post-event-callback but I haven't used it myself. > One of the pieces of complexity that re-frame does introduce is that stuff is held in variables defined by the framework, as opposed to everything being held in variables I define myself. The location itself does not change the complexity. So it doesn't matter whether your state is in my.project.db/state or in re-frame.db/app-db. > I learned ClojureScript because of the cool tooling and observability stuff you have access to with the REPL so this is important to me You have access to re-frame's internals from within a REPL just fine, including app-db. Nothing is hidden, explore away. > are you not supposed to use local state (e.g., useState) in re-frame at all? The rule of thumb I find the most useful is that anything you consider to be your app's state should go into app-db. It enables things like global spec checking, tracing where you have all the data, global rollback in case of failures, state restore, etc.

Kimo 2023-08-07T11:53:19.088159Z

Hey all, thanks for the in-depth discussion. > are you not supposed to use local state (e.g., useState) in re-frame at all? It's conventional to use component-local state by default, and external state when provided. Re-com does this. For instance, here we pass a subscribed value to the https://github.com/day8/re-frame-10x/blob/854b7203827f3b5bed44d252f22724d96f57f5bd/src/day8/re_frame_10x/panels/settings/views.cljs#L97 prop, and a dispatch call to the https://github.com/day8/re-frame-10x/blob/854b7203827f3b5bed44d252f22724d96f57f5bd/src/day8/re_frame_10x/panels/settings/views.cljs#L99 prop. That's all the refactoring you need, to start using global state in place of local.

Kimo 2023-08-07T12:10:18.916949Z

> Shouldn't I avoid the mental cost of the framework This point is super valid. Personally I use plain clojure & reagent for most things, and I don't see the complexity explosion others describe as inevitable. Like you say, it's nice to have your own global state, your own pub/sub registry, etc. You can also find an a-la-carte library for each individual design pattern used by re-frame. I think https://github.com/lambda-toolshed/papillon looks really cool. I think re-frame really pays off when you start collaborating with a team, or with your distant past selves. There's very little you need to learn before you can add, remove or even rewrite existing features.

Kimo 2023-08-07T12:20:10.494179Z

@nnnsadeh, if you do take the re-frame route (or re-fx etc.), I'd love to see what mental model works for you, to encapsulate all of re-frame's concepts. Consider posting a write-up here, or a PR on the official https://github.com/day8/re-frame/tree/master/docs. I'm working recently to clarify the explanations and the visual layouts in all the docs.

Nim Sadeh 2023-08-07T14:55:52.139979Z

> That’s all the refactoring you need, to start using global state in place of local. that’s actually a super mind-bender for me. Say that I have a form and I am keeping the state as a user fills it in. In re-frame, would I put that state (pre-submit) in global store? The issues I foresee happening are: 1. Needing to maintain/keep track of state when elements are removed from the DOM. When using local state, that state disappears with the element. In theory without cleaning up this state might you have a memory leak? 2. Collisions - every time I build a form I’d need to figure out a good descriptive name for the location of the state (e.g., :task/task-id/sidebar/assignee/name for a tasking application, for example). Now I also have to make sure that no other components reference that state by accident? 3. Re-use - using local state in components I can re-use the same for, for example, in multiple places for UX reasons. With global state I now need to consider interactions. E.g., if someone opened a form in one place, filled it out a little bit, and then opened it somewhere else, what’s the expected state? > Consider posting a write-up here Already planning on it!

p-himik 2023-08-07T15:22:55.819999Z

> In re-frame, would I put that state (pre-submit) in global store? Depends on whether or not you consider that state to be a part of your app's state. 1. DOM is driven by the state, so you're in total control. When you do something that removes the form, you can also do something that removes the data from app-db, if you want. If you nest components, you should also nest the state to help with this. And you're right that it is a memory leak if you don't clean it, but not all memory leaks are equal. E.g. leaking a few kBs for the whole UI (but also preserving input that has been entered but not saved pretty much automatically) is one thing, and leaking data constantly on every interaction is another 2. If a component is global, just use keywords that start with :: and some descriptive name. If a component can be reused in many places with different states, you can use gensym or something similar. I prefer to use my own ID generator that creates vectors like [:the-component 7], then I just pass that ID around to use it in events and subscriptions 3. Depends on the implementation. Reusing components when there's global state is not a problem that's been solved in an ergonomic way. There's an issue for that in the repo, and pretty much every state management library in the JS world suffers from it, AFAICT. But I find passing around paths or IDs to not be that big of a deal to justify moving away from global state

p-himik 2023-08-05T20:50:40.599509Z

With core.async there's no global state and everything that's going on is not inherently observable and instrumentable. The common parts is the ability to pass data around, everything else is pretty much different. I'm not sure what magic in re-frame you're talking about. IMO it's all fairly straightforward. The implementation is also simple and very concise, with most of the lines being docstrings. It's OK to have issues with something like this at the start, but I'm sure you'll see how simple it all is once it clicks.

2023-08-05T20:55:59.299079Z

Would some example projects help? We could probably link a few to you that would demonstrate a relatively simple re-frame app. It might put your mind at ease to know that you don’t have to understand coeffects and other topics right away

p-himik 2023-08-05T21:01:22.305079Z

Docs link to the TODO example. I don't think it shows everything that exists there, but it's 80% there.

hifumi123 2023-08-05T22:32:15.029979Z

this blogpost may help too https://dawranliou.com/blog/re-frame-effects-vs-coeffects/

Nim Sadeh 2023-08-08T14:21:25.389199Z

> Reusing components when there’s global state is not a problem that’s been solved in an ergonomic way. It has - local state when appropriate > paths or IDs seems like you’re about to couple the state and the UI in this approach. You’re literally encoding the view into the model, when the framework’s goal is to de-couple them. I imagine that a re-design (common where I’ve worked) would be a nightmare > When you do something that removes the form, you can also do something that removes the data from app-db, Again, you’re being forced to couple the model and the view and pay for it, too I’ll probably end up using re-frame because the app template I cloned has it, and I can probably figure out how to use it to integrate with GET/POST requests and WebSockets pretty easily, but I’ll definitely still utilize local state liberally when appropriate

p-himik 2023-08-08T14:27:57.660289Z

> It has - local state when appropriate You're offering a solution by neglecting the premise. I wouldn't call it a solution at all. > seems like you’re about to couple the state and the UI in this approach How does 8 or [:panel 7] couple anything? I've used this approach for around 5 years now, no nightmares in sight. Prior to that, there were. > you’re being forced to couple the model and the view and pay for it, too The view is a slave of the model in re-frame's world. "Uncoupled" is not a magical thing that helps solve problems. You can uncouple some things, but there's a limit, and at that limit the only thing you can do is to move that coupling to some other place.

Nim Sadeh 2023-08-08T15:08:06.611569Z

The view renders the model. By coupling I don’t mean coupling the data - that’s something you want - but literally coupling the implementation. If the shape of the datastore is dictated by the design of the view (which is my understanding based on some comments here - enlighten me if I am wrong), that’s hard to refactor if you’re re-designing your app. It also couple the view and model in two directions - instead of just “view renders model” (Model -> View) you now also have “Model models View” (View -> Model).

p-himik 2023-08-08T15:16:47.147219Z

> The view renders the model Not according to how re-frame sees things. > the shape of the datastore is dictated by the design of the view No. The view puts some requirements on the data itself and makes some particular data arrangements easier to work with. But it doesn't dictate anything. Subscriptions and events fully decouple views and events. Views don't even know there's a central data store, or a data store at all. Your replies suggest to me that you'll probably benefit a lot from reading the re-frame docs, even if you have already done so. I myself have read them from cover to cover more than once before I had a full picture.

hifumi123 2023-08-08T19:23:06.840029Z

I actually find viewing re-frame or react at all with MVC lens is ultimately unhelpful. This isnt Angular which does attempt to implement MVC. in the re-frame world, views dont drive anything, they dont demand any data, etc. a view is rendered because some event was dispatched and caused the view to render. likewise, data in your app-db exists because some event adding such data was dispatched the way I like to model the overall lifecycle of a re-frame is as follows: • URL idenitifes resource • when the user navigates to the URL, that is an event • dispatch an initialization event and use a router that will further dispatch an event after resolving the URL path to something • continue from there

👍 1
2023-08-08T23:09:08.764409Z

There's is an FAQ entry for the original question ... http://day8.github.io/re-frame/FAQs/DoINeedReFrame/