Fork me on GitHub
#shadow-cljs
<
2022-01-11
>
zeitstein09:01:50

Thinking through using local state in grove conditionally. Something like this:

(defc ui-node [local?]
  (bind {:keys [id data] :as props} (get-in @state-atom [:id 1]))

  (bind local-data (when local? (atom data)))

  (bind data (if local?
               (g/watch local-data)
               data))
  ...)
would get me in trouble, https://github.com/thheller/shadow-experiments/blob/master/doc/components.md the hook type changes if local? changes. What I'm trying to achieve: if local?, work with local state (initialized to the value of global state), else work with global state.

thheller14:01:47

yes this would not work. bind has pretty much the same rules as react hooks or the svelte stuff

thheller14:01:51

no conditions, no loops

thheller14:01:53

(bind {:keys [id data] :as props} (get-in @state-atom [:id 1])) this should also never be done. that will only ever deref once so it'll never updates if you change the value in state-atom

thheller14:01:04

local state is also discouraged so in practice this will never come up 😉

zeitstein17:01:31

(bind {:keys [id data] :as props} (get-in @state-atom [:id 1]))
was a stand-in for a Fulcro hook. If I do a watch on state-atom then I'll chain two AtomWatches, which is a no-no 🙂 So, I can move my conditionals to render, though this means that
(bind local-data (atom data))
  (bind local-data-watch (g/watch local-data))
will initialize needlessly in instances where local state is not needed. I would also need to do
(bind local-data (atom (get-in @state-atom [:id 1 :data])))
to avoid the AtomWatch chaining, but that is what I want: I want to read this only on initialization of the instance. It seems to me like local state is the right 'pattern' for what I need (state per component instance). Storing it in a central atom/db for each instance seems like overkill (would also have to handle component lifecycle). This is how I had it set up in Fulcro with initLocalState. Happy to hear suggestions on how to handle it differently 🙂

thheller17:01:31

as I said local state is not recommended. neither in fulcro nor in grove. first maybe explain WHY you want local state?

thheller17:01:45

> Storing it in a central atom/db for each instance seems like overkill

thheller17:01:30

it costs pretty much as much as storing it in a local atom, so it is definitely not overkill

thheller17:01:27

but without knowing what exactly you are trying to do its hard to make comparisons

thheller17:01:50

if this is about handlings forms I have a lot of plans for that but nothing implemented yet 😛

zeitstein17:01:58

> it costs pretty much as much as storing it in a local atom, so it is definitely not overkill I agree, but would also have to handle component lifecycle (add/remove it on component mount/unmount). The use case is: the :data attribute is stored in a central db. Depending on some conditions, instances of a component using this attribute should either: 1. share this value in the central db (i.e. read current value from and update it in central db) 2. or read and store the value only on component instance initialization, then only update the value tied to that instance. Remove the value (wherever it is stored) once the component instance is destroyed.

thheller18:01:06

ok as a general rule of thumb. grove is very much about the concept of "your view is a function of your data". therefore your data drives your components/views. your components should NEVER create data on mount or destroy it on unmount.

thheller18:01:26

instead some part of your data should cause the mount of a component and removing such data should cause your component to unmount

thheller18:01:03

that entire concept somehow got lost in react along the way but it was the promise at the start

thheller18:01:05

it does require rewiring your thinking a little bit but I find it immensly useful personally

thheller18:01:48

of course there is some state that is derived out of your view such as the height of a div or the mouse position or whatever. those are exceptions and bind and local state work for those

thheller18:01:26

everything else should be part of your normalized state

thheller18:01:05

fulcro is very similar in that regard. giving you even more tools to do this such as the forms and uism support

zeitstein19:01:37

It's currently very difficult to imagine how this would be a practical approach in this case, but I'll try 🙂 Thank you!

thheller19:01:31

I mean the same stuff still happens. its just not your component mount triggering it.

thheller19:01:34

you still haven't provided much info on "in this case". would help if you have some sample code. in fulcro is fine or JS or whatever is fine

zeitstein19:01:03

Right, sorry, thought I was being clear. You know the 'case': deep recursive trees and toggling. Now, in certain contexts, toggle state should be shared globally between component instances, in other contexts the toggle state should purely be local and transient. Every component shares the same data/state on initialization (and most of the data is 'global'), but there are slight deviations in behaviour depending on context.

thheller19:01:09

:expanded-in-foo-context true :expanded-in-bar-context false

thheller19:01:25

with your component querying whichever context it represents

thheller19:01:13

:expanded-in-contexts #{:foo :bar} :expanded-for #{[:some/ident 1] [:other/ident 2]}

thheller19:01:23

there is ALWAYS a way to express all of this as data

thheller19:01:15

the goal is that if you have all your data at hand you know exactly what your view/ui looks like. no guessing.

thheller19:01:41

with local state that pretty much becomes exponentially harder

zeitstein19:01:55

Yeah, I understand that and working with Fulcro was so nice because of that. I'm sure there's a way to represent it as data, but it is much simpler (easier? :)) to use local state (in this case). And, in Fulcro, I can always look at the value of that state as well 🙂 The examples you've provided don't work, unless the idents identify the actual component instances. I.e. I have to track :expanded for every component instance. There is absolutely no sharing (in certain contexts). The context simply defines whether the state should be shared globally, not what it should be.

zeitstein19:01:04

So, the way I see it, representing it as data would be, in some global atom, {id-of-component-instance {:expanded true}}.

thheller19:01:28

again .. flip your thinking and stop thinking about components for a second

thheller19:01:29

there is no such thing as id-of-component-instance. there is id-your-created-in-data-somewhere that is passed to your defc component as a prop

thheller19:01:44

the bind queries something using that id, it gets data back and renders it

thheller19:01:10

in your data you remove that id you created and the component also unmounts

thheller19:01:40

but really this would be much more practical to talk about with some code at hand

zeitstein19:01:51

Thanks for your effort, truly 🙂 I'll prepare some code ASAP.

thheller19:01:30

I always design data first. meaning I create some EDN data until its in a shape I'm happy with. then I create something that renders it, maybe tweak it again until everything looks good 🙂

rickmoynihan09:01:50

Forgive my ignorance on cljs issues… but how does one fix warnings like this:

"5.1.0" was required by jar:file:/Users/rick/.m2/repository/com/kiranshila/cybermonday/0.3.139/cybermonday-0.3.139.jar!/deps.cljs
NPM dependency "remark-parse" has installed version "9.0.0"
I confess the relationship between npm/yarn/shadow-cljs/clojurescript and clojurescript deps from mvn etc isn’t exactly clear to me

rickmoynihan10:01:34

I don’t know; it looks to me like that’s a different problem. Mine appears to be saying that there is a version mismatch; not that the dependency is missing.

rickmoynihan10:01:56

I have a bunch of other similar warnings to do with different deps

ingesol11:01:04

Not sure there’s an easy fix. If you are on tools.deps build, you can fork cybermonday and update its deps, and depend on it as a git coord.

rickmoynihan11:01:22

Ok I think I see what the issue is… looks like someone on my project added the transitive npm deps directly to our package.json

thheller14:01:46

you can also upgrade shadow-cljs. that check has been removed for a while now 😛

rickmoynihan15:01:54

😆 It’s funny you should say that; In parallel I’ve been trying to upgrade shadow-cljs; but I can’t move beyond 2.15.12 without getting test failures. I’m not sure what happened in 2.15.13 that is causing them but I’m assuming it’s somehow related to the closure/clojurescript version bump and perhaps I need to do something with the flag :global-goog-object&array :thinking_face:

rickmoynihan15:01:23

Good to know if I upgrade they’ll vanish though :thumbsup:

rickmoynihan09:01:03

Not to mention all the different javascript module systems etc

orestis17:01:19

Is there a way to stop shadow from starting socket REPL and nrepl when in embedded mode?

orestis17:01:22

I'm trying to figure how to have a plain Clojure environment that uses shadow as a library

thheller17:01:25

:nrepl false and :socket-repl false in your shadow-cljs.edn config?

orestis17:01:55

Ah right. I guess I can pass those as args to start! somehow?

thheller17:01:45

(start! {:nrepl false :socket-repl false}) yes but note that pretty much all in shadow-cljs assumes the presence of a shadow-cljs.edn. so if you attempt to make that not a thing you will lose features 🙂

orestis17:01:55

Or probably since the server start! doesn't need the builds, I can pass it explicitly in.

orestis17:01:50

Oh we have a shadow-cljs.edn file with our builds defined, nothing else (apart from the deps which is what I want to take over)

thheller17:01:56

(-> (config/load-cljs-edn)
    (assoc :socket-repl false :nrepl false)
    (server/start!))

thheller17:01:30

that also works, config being shadow.cljs.devtools.config

thheller17:01:42

its what gets called if you call (start!) without args

orestis17:01:46

Yep makes sense. Is there a map with the other defaults ?

thheller17:01:03

(config/load-cljs-edn) has those defaults merged in

thheller17:01:29

gotta go, be back in a bit

orestis17:01:32

Great thanks. For context this is to assist also with Calva Jack in.

orestis17:01:01

No worries I have what I need, thanks!

orestis19:01:07

Ah, turns out you need to let shadow start nrepl because it injects its own middleware into things. I don't suppose that you can do that outside the context of a shadow build, right? Or to phrase it differently, can I pass in command line arguments to nrepl.cmdline such as that would make connecting to a CLJS REPL work?

dpsutton19:01:51

middleware are always added. CIDER adds its own. Here’s how you can start up an nrepl server with cider and shadow middleware:

/usr/local/bin/clojure -Sdeps '{:deps {nrepl {:mvn/version "0.6.0"} cider/piggieback {:mvn/version "0.4.2"} cider/cider-nrepl {:mvn/version "0.24.0"}}}' -m nrepl.cmdline --middleware '["cider.nrepl/cider-middleware", "cider.piggieback/wrap-cljs-repl", "shadow.cljs.devtools.server.nrepl/middleware"]'

thheller19:01:31

yeah something like that. .nrepl.edn also works I believe. just need to add the shadow.cljs.devtools.server.nrepl/middleware middleware ns somehow