This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-01-12
Channels
- # announcements (6)
- # babashka (45)
- # beginners (4)
- # calva (19)
- # cider (2)
- # clj-kondo (10)
- # clj-yaml (10)
- # clojure (25)
- # clojure-boston (1)
- # clojure-conj (3)
- # clojure-europe (34)
- # clojure-losangeles (5)
- # clojure-nl (1)
- # clojure-norway (11)
- # clojure-uk (2)
- # clojurescript (84)
- # cursive (10)
- # datalevin (3)
- # figwheel-main (1)
- # fulcro (1)
- # jobs (5)
- # joyride (25)
- # lsp (17)
- # malli (18)
- # nbb (1)
- # off-topic (1)
- # re-frame (22)
- # remote-jobs (9)
- # scittle (3)
- # shadow-cljs (26)
- # sql (16)
- # tools-build (12)
- # xtdb (44)
I think form validation sucks in CLJS. You can share specs or validation logic, but trying to find the right way to render your inputs and also map your validation errors to each input is a huge pain. The solutions I've seen online so far are really verbose and an error prone when the form changes. Has anyone found a good way to do this?
ClojureScript is a general purpose programming language, and as such does not concern itself directly with form validation. What solutions to form validation do you enjoy with different programming languages, and what is stopping you from using them here?
Avoiding code like this would be a feature I enjoy in other languages. I have yet to see an alternative to this style.
But macros don't work in CLJS very well either.
I would rather just have a way to derive forms from my Malli specs
hey @U042LKM3WCW it is ok read this https://clojure.wladyka.eu/posts/form-validation/ and see this https://github.com/kwladyka/form-validator-cljs https://kwladyka.github.io/form-validator-cljs/
I saw this. It looks cool, but I am using Malli, not spec
> Has anyone found a good way to do this? I am answering you question. The choice is your 🙂
This looks simpler though so maybe I can port it for Malli?
https://github.com/kwladyka/form-validator-cljs/blob/doc/src/form_validator_doc/form.cljs#L16-L25 - this is final fn for form fields generation which can be use on the end as:
(input {:name :email
:label "Email"})
so on the end it is core under the hood, but this is developer choice how to design UI from that. Otherwise there will be always limitations for customisation of UI.
> Avoiding code like this would be a feature I enjoy in other languages. ClojureScript does not stop you from writing bad code. It also does not stop you from writing good code. Handle form validation in whatever way you find most enjoyable, writing just the code you need. Like you would in other languages. Unless you are talking about specific frameworks? Then it is hardly "form validation sucks in CLJS".
I'm not talking about specific frameworks. I've talked to a lot of people about easy form validation that is scalable in CLJS and people don't seem to have a solution for this.
Then those people don't have solutions for form validation. It has nothing to do with the programming language they are using.
No but if the ecosystem hasn't solved for a basic use case like that, it's an issue, whether it's impossible for the language or not
That's what we call unproductivity
"The ecosystem" is what, precisely? You have full access to everything in JavaScript, on top of whatever is in ClojureScript.
You have interop with javascript, but there is, IMO, friction to using things like React hooks with CLJS. Wouldn't form validation be the type of thing we should solve with data and not with JS?
Or is there no value add to CLJS?
Because code sharing validation is a common value add thrown out there
In my experience, form-validation and other form-tooling for frontends are too opaque and/or too invasive. I tend to write my own tooling that works in the context of my specific projects. It’s very easy for me to add validation to a form in my projects using my tooling, but I have not tailored the tooling to “as few lines of code as possible”, because I personally don’t find that to be an interesting metric. “Good way” is not universal. Maybe you should define more clearly what exactly you’re looking for. My impression is that you’re after defining rules, and have a lot happen automatically and/or by convention?
> Or is there no value add to CLJS? I understand that you’re frustrated, but throwing out “burns” like this is unlikely to get you more help.
I am looking for a scalable way to handle validation errors and interdependent form inputs, as well as dynamic forms. Again, my code above was immediately called "bad" but I've heard 0 alternatives. That's not a "burn", it's a genuine question. There are many people in the Clojure community who don't use CLJS because they don't see a reason to, even if they love Clojure. The frustration is when you treat questions like that as an attempted takedown. As a React dev, I am trying to make a simple app with CLJS to explore whether can add anything to my workflow, and the number one thing people throw out all the time is "You can share validation code." But this seems like even this is limited
> I have yet to see an alternative to this style. How about this? And you could reduce it further if you have other components that would benefit from it by extracting some common functionality.
(rum/defc character-sheet < rum/reactive
[*character-sheet-state *form-errors]
(let [get-state+error (fn [path]
{:state (rum/cursor-in *character-sheet-state path)
:error (rum/react (rum/cursor-in *form-errors path))})
stat-input (fn [id label]
(let [{:keys [state error]} (get-state+error [::spc/stats id])]
(number-input (name id) label state error)))]
[:div.stack
(let [{:keys [state error]} (get-state+error [::spc/name])]
(text-input "name" "Name" state error))
(stat-input ::spc/str "STR")
(stat-input ::spc/dex "DEX")
(stat-input ::spc/cha "CHA")
(stat-input ::spc/hp "HP")]))
> But macros don't work in CLJS very well either.
Uhm... they do? What do you mean by "don't work very well"?
I could write a macro for the above just fine, but I prefer not to because using functions is perfectly enough here.> "You can share validation code." But this seems like even this is limited "Validation code" != "my particular way of representing the inputs that also happen to use that validation". You can have arbitrarily complex validations via any sort of schema, be it spec, Malli, Schema or something else. And you can share those validations between CLJ and CLJS just fine. In the case of Malli, you can even serialize them and send them over the wire to a client, assuming you stick to some fixed grammar and avoid free-floating functions.
> There is nothing in ClojureScript that limits your ability to do form validation. There's nothing in WebAssembly stopping you from doing form validation either, but again, I'm talking about the ecosystem and not the language. Having an ad-hoc solution on every single form seems extremely unproductive
@U2FRKM4TW already gave you some suggestions to de-clutter your initial example. Your initial example showed the most manual way to get the building blocks with your chosen tools in place. Next step is to write som functions that help you with that in a manner that suits you and your application. I’m not saying you should do this for every form, but you put some work into creating some abstractions that all the forms in your app can enjoy.
I like what @U2FRKM4TW wrote and I was attempting something like that earlier. But as you point out, this is what works for my chosen tools. It declutters, but it's not less complicated. I think what's not working for me about this is this is just one form. There's no way I can reasonably abstract this process for my app as a whole. For every form, I'll need an atom for state, and atom for errors, and I need to pass a set-error callback to make sure the errors atom gets updated. If I add another input to my Malli spec, there's nothing to remind me to update this form, or any other form that might use that spec, because there's no connection between Malli and this form. If I need to dynamically render inputs (and I will), the whole thing will break unless I have some very verbose branching logic.
In my experience form libraries have not been a bad thing in JS. They handle a lot of these things and the features are optional but extensible. I was curious if there was a better macro approach to this than what I'm doing
I don’t know either Malli or Rum very well, but it sounds like these tools don’t well fit your needs, or maybe there are other ways to use them. Personally, all my apps have one atom for client-side state, and I update it via an event-bus. No frameworks for this part. This gives me a lot of leeway to create abstractions around shoveling data in and out of the store etc.
Sorry I didn't mean "macros"
I meant macro vs micro solution
Is your event bus essentially just a channel you're pushing things onto like tonksys chat demo?
Yes, it decouples the consumers from the producers, and allows me to represent actions as data:
{:kind :text
:text "Label"
:value (get-in state [:person/form :person/name])
:error (validate state :person/name)
:on-input [[:assoc-in-store [:person/form :person/name] :event/target.value]]
Some semi-philosophical blabber: Projects can generally be divided into 2 categories - where things remain the same a long time and where there's always some churn and some need to experiment. In the former category, being able to use some magic to quickly create some form is hardly useful - creating a form is O(1) and customizing it later might be a chore if that magic doesn't allow it. Learning that magic might also be not worth it - why would I spend 3 hours learning someone's library and then spend additional 5 hours dealing with its bugs when I can spend 1 hour writing my own thing and then 3 hours maintaining it? Mind that I'm not saying that I'm a priori more clever than that library's author. All I'm saying is that UI+validation+whatever else you can envision for simple forms is so trivial to write that actually writing your own set of util function is much easier, simpler, and maintainable than using someone else's library. And if your forms are not simple, then using someone else's solution is even worse. In the latter category of projects where you need to experiment a lot with forms... I actually don't have anything useful to say as I don't work on such projects. But when you write a proper spec for something, I tend to think that it's not a quick and dirty experiment but rather something more substantial, where you'd like for that code to remain in the code base for a relatively long time. And then the previous section makes more sense.
> why would I spend 3 hours learning someone’s library and then spend additional 5 hours dealing with its bugs when I can spend 1 hour writing my own thing and then 3 hours maintaining it? Having written a lot of frontend code in both JavaScript and ClojureScript AND a fully baked JavaScript validation library, I can certainly sign off on this. The amount of stuff a general purpose library needs is worlds apart from what a single app needs.
An in-app abstraction can also make some assumptions about the world that don’t sit well in a general purpose library (like how data lives in stores, etc)
> Yes, it decouples the consumers from the producers, and allows me to represent actions as data: In the case of a CLJS project with React, would you be pulling off the event bus directly from a component that needs to use it? One thing I'm also using is Citrus, which is a lot like re-frame for Rum. So you have controllers and side effects that keep things separated. But they still recommend passing things down instead of directly subscribing to parts of the store from a component that's 10 levels deep, for example. I've been following that advice since I've been bit in React apps before by letting anything subscribe anywhere. I was curious if you would be doing the same with your event bus or something else. > In the former category, being able to use some magic to quickly create some form is hardly useful - creating a form is O(1) and customizing it later might be a chore if that magic doesn't allow it I do understand what you mean. Libraries can sometimes solve things on the wrong...axis of what you're trying to do. But my experience so far has been this form has taken 5x as long as it would've taken me in React and the odds that I'll need to customize it to do anything special (other than the things I've listed) are extremely low. I can't say I've learned anything while working on it on how I could make forms better in CLJS and that's a frustrating experience
> It declutters, but it's not less complicated While it's just quibbling on my end, it actually is. Complex != complicated. Your solution at the very top is complex but it's also the simplest possible. Anything that involves any sort of generation will inadvertently be more complicated, albeit potentially less complex. > If I add another input to my Malli spec, there's nothing to remind me to update this form If you do want a reminder, you can actually write it yourself. :) But what about situations where you want to add a spec but want for it to never be present in the UI? Or add a spec that needs to be in one form but not in some other? Or be present but with a different UI for some reason? Or in a read-only state? Or... you get the idea. > If I need to dynamically render inputs (and I will), the whole thing will break unless I have some very verbose branching logic. Then, assuming you have your ideal "generate forms from specs" solution, you'd also have to dynamically generate specs. Not saying it's bad per se, just admonishing to think about alternatives as it can potentially get really complicated really fast. Just the other day I watched a video from one of the recent conferences where the presenter talked about the Lisp curse. It reminds that "Lisp is so powerful that problems which are technical issues in other programming languages are social issues in Lisp." That's exactly the case here. The implementation that I'd need to write in a particular project for forms similar to the one that's been described is so easy that I would've spent much less time writing it than I have already spent chatting in this and the neighboring thread. :)
> I've been bit in React apps before by letting anything subscribe anywhere. I was curious if you would be doing the same with your event bus or something else. That's a common recommendation in the re-frame world as well, at least when you have a potential need for such a component to be reusable. However, instead of passing the data or atoms themselves, you can always pass instructions on how to access the data. In the case of re-frame that would be passing down actual sub and event vectors or functions that create them based on some arguments.
> In the case of a CLJS project with React, would you be pulling off the event bus directly from a component that needs to use it? I have one atom. To render the app, I dereference the atom, and send it to a function that prepares all the UI data for a specific page. It then passes data to the page component, which passes data down. All my components are “dumb” components that just render data and emit events on the event bus in response to DOM events. A lot of people seem to work hard to avoid this kind of approach, but I like how predictable it is. There is no network etc in any component. Components are just for rendering. I can inspect the UI data for a single page in one spot. I can solve “systemic” things on the UI data (e.g. interpolating i18n strings) in one central place, I can write regression tests on this data. I generally don’t care about “reusable components” than can be plopped in somewhere and then works without support.
> this form has taken 5x as long as it would've taken me in React
Now imagine there's a magic macro/function in CLJS that generates such forms already. If I knew it, I would be able to write such forms as fast as in React (probably faster because with Lisps I at least have structural editing).
The difference here is not what's possible on different platforms. The difference is what you know and aren't hesitant to do.
As I have mentioned, writing such a macro/function, if you consider it useful and if you know your ecosystem well enough to actually be able to write it without learning anything new, is so easy and fast that you would've done it already. That's why many things end up being written time and time again and never published - because even publishing them is an ordeal that's not worth it. And bringing in new dependencies is always a liability with a non-0 cost.
And in the world of React you already know what you need to know to be able to make that form in a short amount of time.
It's somewhat similar to a person that knows Bash asking how to get the second CLI argument in Babashka. In Bash it's $2
, that's it. In Babashka, it's (second *command-line-args*)
. Atrociously longer. But criminally simpler, all things considered.
> But what about situations where you want to add a spec but want for it to never be present in the UI? Or add a spec that needs to be in one form but not in some other? Or be present but with a different UI for some reason? Or in a read-only state? Or... you get the idea.
In this case I was planning on leaning into how Malli schemas can be made up of other schemas. I wasn't sure how I would feel about doing that down the road, but I was going to experiment.
> Just the other day I watched a video from one of the recent conferences where the presenter talked about the Lisp curse. It reminds that "Lisp is so powerful that problems which are technical issues in other programming languages are social issues in Lisp."
> That's exactly the case here. The implementation that I'd need to write in a particular project for forms similar to the one that's been described is so easy that I would've spent much less time writing it than I have already spent chatting in this and the neighboring thread
I couldn't implement anything we've talked about quickly, and I'm a fairly fast programmer. I have been coding in clojure for a while now and I feel extremely frustrated with CLJS. You pretty much need the REPL, but react components are fairly difficult to work with in the REPL because you can't just evaluate the code. They have local state and you have to put in inline defs to see what those states are when it's mounted. Which is maybe half a step above just putting print statements in javascript with hot reload. Because I feel like the REPL is impaired for CLJS, if I get an error, it can be difficult to track it down. I had an error today where a function was passed another function and called it with overloaded arity. Apparently that's fine in CLJS. I didn't know that was happening for a while because the error came from somewhere else. Maybe this would all be easier if I was using an event bus approach
> That's a common recommendation in the re-frame world as well, at least when you have a potential need for such a component to be reusable.
> However, instead of passing the data or atoms themselves, you can always pass instructions on how to access the data. In the case of re-frame that would be passing down actual sub and event vectors or functions that create them based on some arguments.
I'm not against this, I just hate too much prop drilling. It does get out of hand for me. Doesn't mean I need a heavy weight state management solution.
> A lot of people seem to work hard to avoid this kind of approach, but I like how predictable it is. There is no network etc in any component. Components are just for rendering. I can inspect the UI data for a single page in one spot. I can solve “systemic” things on the UI data (e.g. interpolating i18n strings) in one central place, I can write regression tests on this data.
I think the reason is that prop drilling can obscure a codebase. It's not always obvious at first glance whether the on-error
you were passing down 5 components up is the same one you have 5 components down. It can lead to situations where you have to read your app top to bottom just to see where this function comes from. Not really an issue if you're very familiar with the code
> I feel extremely frustrated with CLJS. You pretty much need the REPL Our experiences couldn't have been more different. :) I am very pleased with CLJS and feel very fortunate that I don't have to deal with React+JS/TS directly. And I almost never use REPL with CLJS. Barely use it with CLJ as well. But I might be in the minority here, dunno. > you can't just evaluate the code. They have local state One of the many reasons for why at least re-frame exists. Whatever you consider a part of your app's state resides in a global store and components themselves query that store in one way or another (making them usable from a REPL) and maybe have some local state that's unimportant (current animation progress, stuff like that). > Apparently that's fine in CLJS What you describe is also fine in JS (where you have to deal with "overloads" yourself) and in TS (that has proper overloads). Just in case, because it seems that a lot of people don't know it - in a browser, you make the debugger break on any thrown exception, both caught and uncaught ones. Immensely useful. > I'm not against this, I just hate too much prop drilling. It does get out of hand for me. Doesn't mean I need a heavy weight state management solution. Re-frame is not heavy-weight though. It's a very lean framework, and so unopinionated that I myself wouldn't call it a framework at all. From my POV, it's just a small UI library. That is, if you squint hard enough to stop noticing that it's written for Reagent which is written on top of React, which is a proper framework. Which is a fair squint when you want to use React anyway. And to be fair, the "prop drilling vs global state" dilemma isn't properly solved anywhere. Not in a single framework there is a solution that would satisfy all. There are many approaches to state and its propagation, all with some inherent vices, there are always trade-offs.
I guess to illustrate what I mean, I'll already have to redo this whole form now
I need Max HP and Current HP. There's no way to enforce that as a requirement with Malli that I'm aware of
Current cant go above max
Maybe I could do a merge of the errors from Malli and custom validation
> There's no way to enforce that as a requirement with Malli that I'm aware of
If so (not familiar with Malli enough to judge), then your requirements supersede your tools. That can happen in any ecosystem, with any tool, including React or even JS.
But feels like you should be able to put that dependency between curr-hp
and max-hp
at the upper level, when you combine both specs under one map?
Here's what a Malli spec looks like. This is the only "upper level" I'm working with.
This is the combined map. I don't believe you can self reference names in the schema and say "not bigger than this"
So I think the only thing I can do is either rewrite it or have a validation function that uses this schema AND extra stuff
You totally can do it: https://github.com/metosin/malli#fn-schemas
Like @U2FRKM4TW says, I think it would be far more convenient and expedient to just code this validation up yourself. I would personally start by defining a data structure to define the form, including validations. Something like:
{:fields [{:field/attr :character/name
:field/type :input/text
:field/validations [[:non-empty]]}
{:field/attr :character/strength
:field/type :input/number
:field/validations [[:non-empty]
[:max-value 20]
[:min-value 1]]}]}
Then you could expand your validation function to handle cases like [:max-value [:attr :character/max-hp]]
for the :character/current-hp
field.
Absolutely. You can of course use specs or malli schemas to implement the individual rules if you want, but this gives you more overall control.
I don’t use specs/schemas for frontend validation at all, really. I prefer checking for specific error cases to give better error messages.
Indeed. And writing these validations yourself, you could ensure good error messages by making the validation cases as specific as you want, or even include the error message directly in the validation definition.
If it’s not a big value-add over just piecing together some data I don’t know why you would do that, though.
A value-add can be perceived or potential. For commonly used spec libraries, the latter is usually quite big. And Malli itself seems to be rather convenient and actually very similar to the :fields
map above. I still haven't used it myself, but I'm a bit hopeful.
I don't really see how I would be able to enforce it with this data structure. When I submit the character and need to validate it, I guess I'll have a map of what I submitted. I'll be looping through this config and running each validation function on it's field. :field/attr
will need to be a vector or we need a :field/children
or something, because some of these are nested.
And then when we provide [:max-value [:attr :character/max-hp]]
as a validation, I'll need a way to conditionally parse whether I'm looking at an array that contains the key :attr
. So already we have a function that needs to loop through the config, validate the shape of the data based on the shape of the parent-child relationship, and then conditionally reads :max-value
to know whether it's referring to an attribute or a constant.
I'm not saying I can't do it, but this is not an example of something I can do quickly, and I'm sure there are a lot of edge cases I'm not even thinking of
I would suggest working towards a point of confidence with the language that you could do that quickly, rather than spending time on all sorts of libraries and frameworks. Or I think maybe these frustrations will only get worse.
Yup, pretty much. You ask “where’s the done-for-me form validation everyone’s using?“. The answer is “it’s in the building blocks the language provides - you’ll need to assemble it yourself”. There’s no instant quick fix to get you there. That is where all the gold in this language is - if you see the potential in the pure data suggested over, and can imagine how to add those features you were missing, that’s pretty much how people use this language. This can’t really be packaged in a library, and it can’t be absorbed in a day.
At least this is why I find Clojure and ClojureScript useful. I also think this explains why you can’t find that “one solution in the ecosystem”. It’s a harder sell for sure, but if you stick around it’ll be well worth the effort.
Even if I was confident I could make this quickly, I'll tell you my experience dealing with exactly these kinds of in-house generic form builders is that you end up having to add more and more onto them until they are an ad-hoc mess that is indecipherable to the next person. Or to the person who wrote it if they don't see it for three months. The point of libraries to handle these things is not to say "look, it's done" but to say "look, it's done, it's been used in production by 10 big companies, it's been tweaked to have a less confusing API per their request, it's battle-tested, it scales and it works." So I can attempt to write this. But I can also see it being likely there will be a piece of functionality that can't be expressed in this structure and I'll need to rewrite this at some point. I've had to do a lot of rewrites already in this project because of the way I thought something could work
Will update when I try this
In that case I would say you have too much confidence in code run by big companies, and too little in your own. 😅 I hope it turns out okay. Best of luck to you!
As a quick side note, I would say part of the trick is to not build a "generic form builder", but to build a specific one, tailored to your use cases. My experience is that this drastically simplifies the code, leading to something that is easier to use and extend, than leaning on something generic that needs to consider all use cases everywhere.
> The point of libraries to handle these things is not to say “look, it’s done” but to say “look, it’s done, it’s been used in production by 10 big companies, it’s been tweaked to have a less confusing API per their request, it’s battle-tested, it scales and it works.” It also supports a metric ton of features you don’t need, and has its API and code complicated by this fact. It will also change even when you’re happy with what you have, sometimes in breaking manners. So it’s a lot of complexity you don’t need - and probably shouldn’t want. Unless you find a library where you imagine using most of its features it’s probably not worth it.
This is especially true with ClojureScript, where your users are often paying for those superfluous features with needlessly bulky js-bundles.
I have been starting to use react-hook-form with cljs (https://github.com/matterandvoid-space/todomvc-fulcro-subscriptions/blob/mainline/src/main/space/matterandvoid/todomvc/todo/ui.cljs) I haven't done it yet, but the plan is to integrate malli schemas similar to something like https://react-hook-form.com/advanced-usage#CustomHookwithResolver (and full https://jasonwatmore.com/post/2021/04/21/react-hook-form-7-form-validation-example) . I don't have it in the above example, but in another project I've taken inspiration from this https://koprowski.it/react-native-form-validation-with-react-hook-form-usecontroller/ to make form context agnostic reusable input components. So using that strategy and combined with the custom resolver it should be possible to just declare a malli schema and pass it to useForm's resolver and then have the errors show up, all declaratively (and setting custom error message via malli's support for that) - I am not sure how this would work with rum though, I'm using helix.
I may move away from Malli for this, but just to show some progress, I wrote a function that lets you write comparisons in Malli a little easier. It lets you pass paths to things you'll be comparing, handler/error functions, where to display the error, etc. Not sure how much time something like this saves me, but I wanted to write it. (Made some arbitrary requirements for this)
(defn reduce-paths-to-args [obj paths]
(map (fn [path] (get-in obj path)) paths))
(defn compare-properties [error-path error-func v-property-paths validate-func]
[:fn
{:error/fn error-func
:error/path error-path}
(fn [x]
(let [args (reduce-paths-to-args x v-property-paths)]
(apply validate-func args)))])
(def player-character-base (m/schema [:and
[:map {:closed true}
[::name non-empty-string]
[::stats
[:map
[::str stat-options]
[::dex stat-options]
[::cha stat-options]
[::hp hp-options]]]]
(compare-properties
[::stats ::str]
(fn [] "Strength must be bigger than Dex")
[[::stats ::str]
[::stats ::dex]]
>)
(compare-properties
[::name]
(fn [] "Name must include the dex number in it, unless dex is bigger than cha")
[[::name]
[::stats ::dex]
[::stats ::cha]]
(fn [name dex cha] (or
(> dex cha)
(clojure.string/includes? name (str dex)))))]))
If you were doing this in React, what would you use? Because I don't have a generic answer in React neither. Btw I agree with you with the point on libraries that doesn't provide only results. A good library is highly tested, optimized and well designed. ClojureScript has a small community and so it has few libraries. But as I said, I don't know your project so just tell me what would you use in React and maybe we can find the closest solution in cljs.
I would use something like react-hook-forms.
Currently I'm doing something a little wacky. I keep trying to have Malli specs as a source of truth of some kind. So I wanted my form state to resemble the map that would be validated by malli. But a nested map isn't great for a form-state. So I made recursive functions for changing map (which follows the spec) to a configuration for rum components. It's still nested after I transform it, But it involves reducing the map to a bigger form with things like :value
, :on-error
, :current-errors
.
I'm also going off what @U9MKYDN4Q mentioned and moving most of my state to a single Atom. I'm handling that with Citrus which is a lot like reframe. I may replace citrus entirely later.
I may be making things way too complicated, but I feel it's a shame Malli has so many tools for working with their data and I'm not using them.
had some time today to put the pieces together in a shareable form: https://github.com/dvingo/malli-react-hook-form this repo demonstrates using a malli schema to validate a form. entrypoint is https://github.com/dvingo/malli-react-hook-form/blob/df988b72545c2a802d761923d16add09aaa15ed5/src/app/malli_react_hook_form/entry.cljs#L38 Live example here https://dvingo.github.io/malli-react-hook-form/