Fork me on GitHub
#untangled
<
2016-05-18
>
currentoor05:05:30

unless i’m doing it wrong there are definitely some subtleties with routing and modals (using CSS animations) when you have optimistic mutations

currentoor05:05:34

i’ve got a working solution in my app using a combination of local state and will-recieve-props (to avoid re-rendering modals and not using putting tempids in the URL, but putting the actual :db/id in the URL when it gets there)

currentoor05:05:15

it doesn’t feel pure but i suppose routing and tempid replace is stateful by nature

currentoor05:05:51

i’ve got three more modals to make in my app then i’d like to add what i’ve done as a cookbook recipe

iwankaramazow16:05:24

@currentoor: I have a router working https://github.com/IwanKaramazow/om-router, I'll plan to submit a PR to the cookbook with it.

currentoor16:05:45

@iwankaramazow: have you tried this with untangled? i thought untangled did not use the read function?

currentoor16:05:01

nice write up by the way, very clear

currentoor16:05:57

also, how do you handle tempid, say I create a dashboard on the dashboard index page /dashboards/index, i want the app to immediately open this newly created dashboard and route to /dashboards/:id but :id will be a tempid initially (because of optimistic mutation) then it will be resolved to an actual :db/id

currentoor16:05:54

so i want the UX experience to take advantage of the optimistic mutation but i also don't want the route history to have a tempid in it

therabidbanana17:05:36

We occasionally see this error in our console when clicking around our untangled app - anybody have an idea of where I'd want to start looking for this? "Uncaught Error: No protocol method IWithMeta.-with-meta defined for type cljs.core/Keyword: :untangled.client.impl.om-plumbing/not-found"

ethangracer17:05:26

@therabidbanana: yes, it’s a mark-and-sweep-missing keyword

ethangracer17:05:56

when you request data from the server, we check to make sure that the server returned data for every requested item in the query. If the server didn’t return data for a given item, but the client has data at that key (say, another client deleted the data at an earlier point in time), then we want to remove it from the client

ethangracer17:05:20

we differentiate a nil value returned from the server with no data returned from the server with the not-found keyword

ethangracer17:05:35

so if you’re seeing an error about it, it’s probably because you’re trying to access data in your client db that no longer exists on the server

therabidbanana17:05:58

So most likely culprit would be maybe a query on the client that accesses a property we don't send back?

ethangracer17:05:33

that’s definitely possible, yup

ethangracer17:05:52

I’ve seen it before when I query for something after resolving a tempid

ethangracer17:05:07

because the request to the server has a tempid in it, but the tempid has since been resolved

therabidbanana17:05:59

Okay - that gives me some avenues to check. Thanks! Is this maybe a case where a better error message would be warranted from the untangled client? I could open an issue for it

ethangracer17:05:17

Possibly, I’m not sure exactly how that would work… maybe modifying the read function to detect finding the keyword to throw a more a more helpful error. Can’t hurt opening an issue, fair warning we’re on crunch time here right now so probably won’t get to it for awhile

ethangracer17:05:40

I agree though, a more helpful error is definitely called for

therabidbanana18:05:17

Oh, yep, definitely looks like we're trying to lazy-load data by a tempid. That probably explains it.

iwankaramazow18:05:47

@currentoor: Untangled definitely lets you use reads, otherwise there won't be any data flowing down you React tree 😉 I haven't thought about tempids and routing. Hmm, I'll tinker with it for a bit

iwankaramazow18:05:39

have you come up with any ideas ?

therabidbanana18:05:33

So my error traces back to untangled.client.data-fetch/load-field being called with something that has a tempid - is that something that seems like an edge-case the load-field function should be able to handle? I can do a type check on our db/id to make sure it's not a om.tempid, but seems like something that might warrant a more generic solution?

tony.kay18:05:51

So, just to be a devil's advocate...and I know this could start a heated debate: have you considered that when building a "desktop-like" experience, routing might not even make sense. Can you "go back" in Excel? IntelliJ? Emacs? etc etc etc. It adds a hell of a lot of complexity to the code base, which can hurt you in a lot of ways, and really isn't something users "can't live without". When is the last time you used the back button or a bookmark in gmail?

tony.kay18:05:38

Routing in the sense of "paginating an interface with tabs" seems ok

tony.kay18:05:54

just a personal opinion

iwankaramazow18:05:05

Hmm, that's some solid food for thought 😄

tony.kay18:05:08

but I often find us bending over backwards to implement stuff that really isn't used

iwankaramazow18:05:37

I know you fond of union queries for tab ui's

iwankaramazow18:05:52

do you use them a lot at work?

tony.kay18:05:52

only because it is easy to trace

tony.kay18:05:21

yeah, unions are the only way we tab interfaces...easy enough to join up with routing as well...just change a set of idents

tony.kay18:05:38

I see them as like little hobby train switches in my head 😉

tony.kay18:05:49

switch that one left, and that one right, etc

iwankaramazow18:05:40

I tried implementing routing with union queries, but I often faced a situation where there wasn't an ident available, so I settled on joins

tony.kay18:05:02

what do you mean "not available"?

tony.kay18:05:21

as in you needed to hot-load something

iwankaramazow18:05:48

I think it were queries with mostly plain keywords, no idents in the mix

tony.kay18:05:52

joins is not so great because you have to query it all every time..so for a large UI can do a lot of extra work per frame

iwankaramazow18:05:33

Also those joins disrupt the query structure a lot

iwankaramazow18:05:55

I always assumed it was more of a theory vs practice thing about Om's queries

currentoor18:05:25

@tony.kay: i agree about not going overboard with routes but since my app is reporting software we need some links, for example a scheduled report goes out weekly. users will get a link to a particular dashboard once a week

currentoor18:05:20

routing in an app like gmail does sound pretty useless though

iwankaramazow18:05:38

If we continue over-engineering, there'll probably be some kind of history we need to keep about those links

iwankaramazow18:05:58

Not so pretty, but I don't see another way atm

tony.kay18:05:18

@currentoor: Yep. We do exactly that sort of thing in one place...Not saying it is useless 🙂

tony.kay18:05:55

ours is a link to take a survey..but really we do it as a query parameter instead of routing...just pull it in and integrate it with initial load

tony.kay18:05:34

NOTE to channel: Just pushed a recipe in the cookbook for doing the large paginated list (loading pages on demand)

tony.kay18:05:40

@baris: @darrellesh @pithyless One of your requested recipes is done.

tony.kay18:05:05

Websockets example with server push was also added by @mahinshaw

tony.kay18:05:06

The websockets support comes from untangled-websockets. A new add-on library. NOTE: Websockets has some distinct limitations. The most glaring is an on-wire message size limit.

ethangracer19:05:13

@therabidbanana: we have the same problem, only when using load-field. We haven't found a solution yet but are working on it

tony.kay19:05:14

https://github.com/ptaoussanis/sente/issues/117 may actually be solved. We are using http-kit underneath, but that could easily be changed to fix that

tony.kay19:05:10

@therabidbanana: So, the built-in remapping of tempids should affect all of app state, including the network send queue. Are you holding onto the tempid and doing some kind of later submit? We could (should) add an error message to load-field to help with this.

tony.kay19:05:58

because obviously it doesn't make sense to try to load a field on something the server doesn't even know about yet.

therabidbanana19:05:04

We're calling load-field within the newly created dashboard widget's componentDidMount, if the data is not there already. As far as I know we're not holding onto it in some way, so the send queue should get rewritten from the sounds of it

tony.kay19:05:50

newly-created how? Via optimistic update?

tony.kay19:05:03

so yeah, the response would not be back yet

tony.kay19:05:07

that is not the place to do it 🙂

tony.kay19:05:18

in fact, I would never recommend using lifecycle for loads

tony.kay19:05:27

well...maybe not never 😉

iwankaramazow19:05:33

why avoid them?

therabidbanana19:05:35

Where's a better place for it then?

therabidbanana19:05:46

It definitely did seem potentially hacky

tony.kay19:05:53

Because you'll trigger loads as you take them off screen and put them back

tony.kay19:05:08

unless you then embed crappy state checking logic

baris19:05:13

@tony.kay: awesome….many thanks to you and the hole untangled team

therabidbanana19:05:31

We did end up embedding some crappy state checking logic. 😄

tony.kay19:05:05

What you want is a follow-on read

tony.kay19:05:35

the transaction should have the mutation, and a read. You can use the app/load mutation in your transaction. Not as API-nice as load-field, but not hacky

tony.kay19:05:52

(follow-on REMOTE read)

tony.kay19:05:13

(transact! this '[(make-thing) (untangled/load ...)])

tony.kay19:05:28

See the implementation of load-field

therabidbanana19:05:43

Twist here - we also don't read these with initial app state (large reports we only want if you open a specific dashboard)

therabidbanana19:05:07

So in that case we'd also want to transact the untangled/load in the transition to that page?

tony.kay19:05:33

you can also trigger loads from another mutation. See load-data-action and load-field-action.

tony.kay19:05:55

The new recipe in the cookbook for paginated loading uses this latter approach

tony.kay19:05:30

So, you could place your app-state checks into a mutation, where they aren't hacky

tony.kay19:05:46

again, the recipe shows this...it short-circuits cache loading based on presence

therabidbanana19:05:57

Cool - I'll take a look

tony.kay19:05:03

@iwankaramazow: So, for all the reasons listed above...but for more clarity: loads happen for two real reasons in my view: initial app load, and user-triggered interaction. The former is covered by started callback in client, the latter can be handled via the options I just listed.

tony.kay19:05:14

Out-of-band interactions (like server push) can also be handled without using lifecycle...see the websockets recipe

tony.kay19:05:07

Also keeps your UI as a pure function...lifecycle messes with that, and is generally only desirable when integrating with stateful libraries like d3

therabidbanana19:05:59

load-field-action definitely seems like something I was looking for - ended up building an (untangled/load... ) transaction with a focused query by hand, to happen after another mutation. This will definitely be helpful - thanks for pointing it out.

therabidbanana20:05:08

@tony.kay: looks like it wants me to override remote - which results in my original mutation not triggering if I try to use load-field-action inside a mutation I want to hit the server (om/transact! this [(widget/new-data-stream ~%)]) My thought on that is to just have two mutations, since I want new-data-stream to reach the server (om/transact! this [(widget/new-data-stream ~%) (widget/load-data-stream ~%)]) My understanding of how it works makes me think that's the right way to do it, but just wanted to confirm.

therabidbanana20:05:56

Now that I've asked my question, I guess I like the second approach anyway because it makes it easy to get rid of that lifecycle method and reuse the "load" transaction instead.

ethangracer20:05:30

@kenbier thanks, looking at the code again that should be a very simple fix.

tony.kay21:05:09

@therabidbanana: Sure, any number of mutations can occur (in sequence) and the order is preserved

tony.kay21:05:33

The networking layer of untangled ensures one-at-a-time semantics unless you use the background option of load-*

therabidbanana21:05:48

So I guess the follow up to that - now that I have a transaction I want to reuse - how crazy do you think it would be to call om/transact! inside another mutation? (We have route hooks already set up to be transactions - so ideally our "dashboard/show" mutation could cue up a few follow on lazy load transactions if it needed them)

therabidbanana21:05:11

(Functionally it seems to work, can't decide if it's just a different kind of hacky to call transact! in a mutation vs the componentWillMount approach I'm replacing)

ethangracer21:05:56

calling transact! inside of a mutation could get kinda screwy in terms of the render lifecycle

ethangracer21:05:08

because transact manages what components are rerendered

ethangracer21:05:33

you could manually call the mutation but… that’s kind of ugly

tony.kay21:05:36

right...you don't do that...use a sequence of mutations in a top-level transaction

tony.kay21:05:58

(transact! this [(a) (b) (c)])

tony.kay21:05:14

if you have conditional logic (or non-DRY stuff), feel free to compose real functions together to make the mutation

tony.kay21:05:26

transacting against the reconciler causes full query/re-render, and could also lead to nasty things like mutual recursion that could blow your stack in a way that is hard to trace (if using from within mutations like that)

tony.kay21:05:04

also remember that transact will trigger both a local AND remote (for each remote) parse.

tony.kay21:05:32

BTW, it is ok for your :action to call load-data-action multiple times. Each call adds a load to the queue.

therabidbanana21:05:32

Hmm... I'll have to rethink our routing setup a bit then.