Fork me on GitHub
#helix
<
2021-07-30
>
raspasov00:07:11

@hiskennyness in my opinion and from experience with React(Native) in the last ~6 years, keeping all the app state in one atom (and yes, only one), is a good thing. I don’t use React context or any frameworks or patterns like FRP; I just pass props down from the root. All data management is simple, and easy to reason about. That way you avoid state inconsistencies and hard-to-reason-about bugs. I almost never run into performance issues. If you do have perf issues, selectively add a useState hook to a component that is being updated multiple times per second. That’s when I run into the bugs and “what the hell” moments 🙂 It’s almost inevitable with multiple source of truth, and it gets exponentially worse the more sources of truth I add.

raspasov00:07:01

To me it feels like Concurrent mode is trying to solve a problem for a very big app (think the FB website, not even the mobile app, because there’s only so much visible on the screen in one moment), with dozens/hundreds of components, all developed by separate teams, where each component is doing its own state management and they want to avoid any and all state centralization. If your app is not like that, I’m not sure if you’d see a benefit.

lilactown15:07:06

IIUC concurrent mode is more about UX than state management. Concurrent mode allows us to reorder updates based on priority, so that we can e.g. update state based on key presses immediately before other less important things, which you can't do without bugs if you store state outside of the React tree.

👍 4
raspasov00:07:41

Also, one thing that JS does not have going for itself is efficient immutable data structures. You get a situation where you have 10 teams working on the same frontend app, some are using Immutable.js let’s say and doing efficient rendering, some are using plain JS arrays/objects and re-rendering everything on any and every change. It’s a mess. Concurrent mode might help a case like that.

kennytilton01:07:42

Yes, @raspasov, mobile apps are definitely simpler, so a lot of issues are avoided. The problems I have seen with re-frame, eg, are in web apps that once were desktop apps. Then the global state tends to trip people up: I see a lot of "leftover" data bugs, because data was not cleaned up after processing. re-frame and redux brag on beautiful state flow, but it is a globally scoped state. Dynamic scope is how things work, so there is a fundamental conflict that gets exposed as apps grow. Is global state bearable for small apps? Sure. But why deal with it when we could have dynamically scoped data?

raspasov01:07:53

@hiskennyness I’m not sure that mobile apps are simpler 🙂 They are just… different. There’s less shown on the screen at any given time, but they do have their own complexities. When you say dynamically scoped data, what do you mean by that? How do you solve the problem of synchronization and communication between those different “scopes”?

kennytilton06:07:07

"There’s less shown on the screen at any given time" That is part of it, @raspasov. Fewer ways for the user to interact means less complexity to manage. Also, mobile apps follow a "modal" model, vs the "modeless" model of good desktop apps. On the desktop I pick an object and then any action I have in mind, and the interface shows me which are allowed. On mobile I have to click the hamburger, click "Delete", and the whole app is in "Delete" mode, and I stay there until I am done with that. Lousy U/X, simple coding.

raspasov06:07:04

Once you get past the basic case, there’s a ton of complexity/mutability around multi-touch, for example. Even a simple photo viewer component with all of its gestures is a pretty complex beast. I’ve done both React and React Native and I would say IMO React Native/mobile is harder to get right, at least for me.

kennytilton06:07:34

"When you say dynamically scoped data, what do you mean by that?" In Matrix, everything is a tree. Esp. the view, with widgets part of components part of panels etc etc. And the shocker: the view tree is also a state tree. (Not sure hot reloading is going to like us.) As a plus, any node in the tree can see any data, but searches the tree inside out, "inheriting" data from containing nodes. ie, a node's location in the tree determines its context. All very orderly and easy to think about. Redux/re-frame's global, flat stores discard that context.

raspasov06:07:26

I think I saw the Matrix project somewhere, link?

kennytilton07:07:55

Project is a bit scattered. Here is the CLJS version: https://github.com/kennytilton/matrix/tree/master/cljs/matrix The JS version is alongside that in the same repo. Best write-up starts with this: https://github.com/kennytilton/mxtodomvc That includes the "Data Integrity" bit at the very end.

kennytilton07:07:07

I am working on a RN version with the great help of Helix, then I plan to clean everything up, document, and produce a tutorial sequence.

raspasov06:07:52

Not to mention that the perf. requirement for mobile are generally higher; the slightest touch delay will be felt by the user.

kennytilton07:07:55

"How do you solve the problem of synchronization and communication between those different 'scopes'?" Synchronization requires a bit of interesting code. 🙂 MobX has a solid approach, this is a specification mine: https://tilton.medium.com/the-cells-manifesto-b21ed10329f0. Scroll down to "Data Integrity" for the detailed requirements. Communication works by letting any node in the graph see any other node, but by navigating to other nodes hierarchically so data is discovered in a naturally scoped manner. Less mysteriously, a node can see its parent, and has only one. As long as I search up, I see only my context, following nesting order. So it is not really communication, it is more omniscience. Yes, this is the opposite of today's obsession with purity. Works great. Unlike purity. (See poor ReactJS's long history of trying to allow "pure" views to see the outside data they in fact do need.)

raspasov07:07:49

I think I understand… I can see how this can be beneficial if your data structures in the language are mutable (this was originally for Common Lisp, yes? I haven’t used Common Lisp myself) . I’m not sure if this approach is needed/useful with Clojure(Script). Immutable data are values. We can share data freely between components without any issue and have a simple (fn [data] (view …)) model

raspasov07:07:42

Have you looked at SwiftUI? They have done some pretty cutting edge stuff for a mutable object situation. Was nice to use, but I only have ~1 week of experience with it. Definitely worth a look if you’re interested in different UI/frontend approaches.

kennytilton08:07:42

Mutability matters at a different level of abstraction. At the application level, as even re-frame says, mutation is how the application moves forward. That is true no matter what language we use. The full re-frame take on mutation: "Now, to a functional programmer, effects are scary in a xenomorph kind of way. Nothing messes with functional purity quite like the need for side effects. On the other hand, effects are marvelous because they move the app forward. Without them, an app stays stuck in one state forever, never achieving anything. So re-frame embraces the protagonist nature of effects - the entire, unruly zoo of them - but it does so in a controlled and largely hidden way, and in a manner which is debuggable, auditable, mockable and pluggable."

kennytilton08:07:09

Matrix decomposes almost the entire application into s = (f s). View is state, too, after all, though conventional wisdom tries to relegate it to second-class citizenship. If one looks at how Matrix achieves all-functional-all-the-time, with formulas deriving all interesting application state, one sees mutation in the implementation state, not the application state. These are the different levels of abstraction I mentioned earlier.

orestis08:07:20

Hey there @lilactown, would you be interested in making someone a maintainer of hx? We use it actively in production and we'd like to be able to make small tweaks without bothering you.

lilactown15:07:54

I think I mentioned this on defn recently 😄 was definitely thinking of you @orestis

orestis16:07:22

I'm glad to take this on, let me know what should be done... I guess commit bits to the repo but perhaps also building and cutting new releases in clojars? I'm clueless on that front...

lilactown17:07:17

who should I transfer it to?

lilactown19:07:38

@orestis I'll initiate the transfer to the github user orestis if that works for you

orestis19:07:08

that should work

lilactown15:07:06

IIUC concurrent mode is more about UX than state management. Concurrent mode allows us to reorder updates based on priority, so that we can e.g. update state based on key presses immediately before other less important things, which you can't do without bugs if you store state outside of the React tree.

👍 4
lilactown15:07:25

you can also start rendering a screen and add a transition in between if it takes too long, which again is very difficult to do without bugs if you are controlling what is on the screen outside of the React tree

lilactown15:07:16

I actually am working on this problem right now: we have a page that can sometimes take over 1s on initial render, so we want to show a spinner while it's churning on that new screen. But I don't want to show the spinner if it takes less than say, 200ms. AFAICT it's not possible to do that with synchronous React, since I need the ability to interrupt the long render to show the loading transition if it takes too long.

raspasov21:07:16

1s to render is a lot, I agree. Do you think that is because there are many components on the screen? Or because there’s a many updates/mutation that are each done separately?

raspasov21:07:30

I assume there might also be some HTTP request/de-serialization going on that is using up that one JS thread…

lilactown00:07:33

The app loads a lot of data and processes it in a re-frame subscription. That subscription gets recompiled anytime someone navigates to the page

lilactown00:07:37

And that processed data is used to calculate and render a large data grid on the screen, which also takes awhile

raspasov04:07:38

“recompile” - does that happen on the client or the server?

raspasov04:07:50

Have you explored/tried web workers?

lilactown13:07:55

"recompiled" I meant recomputed 😛 it happens on the client

👌 4
lilactown13:07:14

no to web workers. that would require a lot of work

👌 4
lilactown13:07:59

a lot of it is bad code. I've managed to in other areas slim a similar computation down from 1s to <200ms

👌 4
lilactown13:07:51

but it's an extreme illustration of a class of problems, and one of the solutions to it is to leverage concurrent mode to provide a much better UX when you have a slow path in your app

👍 6
lilactown15:07:37

the whole local vs global state really comes down to like you said, performance & organization. then there's whether you store the state inside or outside the tree, which also has performance & organization, and also UX implications w/ concurrent mode

lilactown16:07:02

what I really want is an external store that can participate in React's scheduler, so we can have our state outside the tree and coordinate updates with React's VDOM. that's what the vision of https://github.com/lilactown/flex is

nice 2
raspasov21:07:34

This sounds like a good idea! You get fully consistent data state, and something like “eventually consistent” render state within milliseconds, which is fine for majority of cases.

lilactown19:07:01

yeah. right now it's very slow, so it's more like 100s of milliseconds 😂

👌 4
lilactown19:07:12

but lots of room to optimize too

🎩 4