Fork me on GitHub
#reagent
<
2019-10-10
>
pesterhazy15:10:38

I'm running into an issue with rerendering. The code looks roughly like this:

pesterhazy15:10:40

(into [:div]
           (for [item items]
             ^{:key (:id item)}
             [item-ui item]))

Roman Liutikov15:10:56

arghh I was just typing this 😄

😂 4
lilactown15:10:47

skip the into

pesterhazy15:10:48

The bug is the following:

pesterhazy15:10:43

- View1: the component shows elements A, B - View2: the component rererenders, now it shows elements C, D (so far so good) - Back to View1: the component shows C, A, B (not so good!)

pesterhazy15:10:31

Expectation: - Component shows A, B again Actual behavior: - Component shows C, A, B. That is, it shows a remnant of what was previously shown

pesterhazy15:10:54

In fact if you switch back and forth between View1 and View2, you can reliably add more and more Cs

pesterhazy15:10:16

We've narrowed down the problem to this specific view function

pesterhazy15:10:44

Two funny observations: - If you look at the React devtools in the final state, there you only see A, B, even though C is clearly in the DOM - If you remove the 3rd line (`^{:key (:id item)}`), we see the expected behavior

pesterhazy15:10:39

Anyone have an hunch what the issue could be? Feels like it should be: (a) a React bug, (b) a Reagent bug or (c) a misunderstanding of React keys work on our part

pesterhazy15:10:29

@juhoteperi have you seen this kind of issue before? I may be missing something obvious

p-himik15:10:53

What happens if you place the loop directly into [:div ...]? Without realizing the lazy seq, as lilactown suggested. Or maybe even into [:<> ...] if you don't need the div element.

Roman Liutikov15:10:16

still happens if key is present

pesterhazy15:10:05

yeah, same thing unfortunately with [:div (for [item items] ^{:key (:id item)} [item-ui item])]

Roman Liutikov15:10:28

the fact that number of DOM nodes and React element instances is different says it’s a bug in React, imo

lilactown15:10:30

are your keys not unique?

Roman Liutikov15:10:37

they are unique

pesterhazy15:10:25

I'm verifying this right now

pesterhazy15:10:35

OK, so I'm printing our keys now, and lo and behold, in View2 we have 1 duplicate key

lilactown15:10:57

here’s a repro

p-himik15:10:39

My personal rule number one of debugging Reagent stuff: print everything, even if you think it's not needed.

lilactown15:10:54

by adding a :key, you are telling React to optimize the re-rendering of the list. but if the keys are not unique, it will confuse React

lilactown15:10:13

is this list actually dynamic enough to warrant using keys?

lilactown15:10:55

e.g. are you adding and removing individual items from the list? or do you just blow it away and re-render a unique list of items (e.g. A + B, or rendering C + D)

Roman Liutikov15:10:00

@pesterhazy we also are seeing constant increase of Dom nodes by a random number, could it be that 1 duplicate key is causing this?

lilactown15:10:42

the repro I pasted above has that same behavior @roman01la

Roman Liutikov15:10:51

Oh nice, thanks!

pesterhazy16:10:57

@lilactown that's very helpful, thanks

lilactown16:10:18

for ease. neat finding

pesterhazy16:10:55

how do I run shadow?

lilactown16:10:12

run npm install in the repo and then npx shadow-cljs watch app

pesterhazy16:10:51

So I did know that keys need to be unique, but for extra context a few things confused me - misconception about our own data, assuming that there wasn't a possibility of dupes :face_palm: - the fact that there's no warning or error but instead faulty behavior. This may be because we're seeing this in a prod build (the problem only occurred with prod data)

Roman Liutikov16:10:53

I'd keep the warning in prod, consequences are super bad

pesterhazy16:10:51

@lilactown your minimal repro case is neat

🎉 4
juhoteperi17:10:20

@pesterhazy & co. Good to see this solved. Extra elements in the dom is usually result from non-unique keys. Reagent doesn't check uniqueness of the keys, as React already does this (`Encountered two children with the same key`), on the dev builds.

👍 4
pesterhazy17:10:17

Yeah I remembered that there was a check. But the problem was reproducible only in prod, where the checks are elided

pesterhazy17:10:26

Not really surprising. Gnarly bugs often only surface with real-world data

juhoteperi17:10:08

Yeah. Would be cool to have options in Reagent to control checks such as this more closely, as having them enabled by default is not feasible for optimized builds. Maybe at some point.

juhoteperi17:10:38

Quick idea of what might be possible to implement to control checks and also Hiccup transformations (similar to Hicada compiler options):

cljs
;; Take Renderer object as optional parameter for render & as-element.
;; The renderer controls "everything" done by Reagent to convert Hiccup,
;; keep track of RAtoms etc.
;; If different options need to be provided to some components, as-element can be
;; used to select different renderer for a tree.

(def renderer
  (r/renderer
    {:checks {:key-uniquenes true}
     :hiccup-options {:custom-elements {:text-input rn/TextInput}
                      :key-to-element ...
                      :prop-name-fn ...}}))

(defn main []
  [:div
   [:h1 "Example"]
   (r/as-element renderer-2 [foo])])

(r/render renderer [main] el)

lilactown17:10:11

that would be pretty great

pesterhazy18:10:58

That’s pretty cool, yeah

pesterhazy18:10:58

In this case though it may not have helped because in prod we would turn that check off as well

pesterhazy18:10:25

Whereas in dev, React would have already caught the issue

pesterhazy18:10:44

As for custom elements, where do you see the advantage over referencing the component function?

pesterhazy18:10:33

The component is more direct than the keyword and with functions, the compiler warns you about typos

juhoteperi18:10:00

I don't see really advantage in such simple case, it was just quick example what could be there. Probably custom elements could be implemented through (Hicada options) transform-fn or create-element option or such.

juhoteperi18:10:23

Having some customizable transform step could be useful for example when porting web code to React native. I recently implemented such case in one project where we had 3000 line form page in web, and I was able to just take the whole code and put in a React-native app by creating function that converts Hiccup forms from DOM elements to RN components. It worked fine, but I had to call the function in several places.

lilactown18:10:35

@juhoteperi you mention hicada, which does compile time parsing of hiccup to JS interop. are you thinking of something similar with reagent in the future?

juhoteperi18:10:03

@lilactown Compile time parsing is impossible with Reagent API. If one wants to do it, one can wrap hiccup in Hicada macro calls. In theory I'd like to have an optional macro in Reagent to do the same, but not sure if it is feasible.

lilactown18:10:47

yeah, I know it’s impossible with current API just wasn’t sure if that’s where you thinking of migrating towards

juhoteperi19:10:07

Maybe the Renderer thing could be implemented in Cljc and could be shared for macro compile time and runtime.

lilactown19:10:54

what’s the biggest blocker to being all compile-time?

lilactown19:10:18

reaction tracking?

juhoteperi19:10:43

Hiccup compiler part doesn't need to care about reactions.

juhoteperi19:10:21

It has to implement a simple Clojure compiler so it can transform hiccup inside for loops and such forms.

juhoteperi19:10:59

That would break e.g. if using core.match macro: (html [:div (match [a b] [0 0] [:div "0!!"])]) because it doesn't see the hiccup form inside unknown match macro.

lilactown19:10:14

right. I think it’s a case of trying to be a bit too clever

juhoteperi19:10:56

Alternative is that one has to wrap nearly every Hiccup form in a macro call: (html [:div (for [i (range)] (html [:div i]))])

lilactown19:10:15

one thing I’ve been experimenting is using tag literals

lilactown19:10:02

#hic [:div
      (for [i (range)]
        #hic [:div i])]

lilactown19:10:16

it reads a bit nicer

juhoteperi19:10:23

Yeah, if every form was tagged in some way, the macro could just walk the whole form and wouldn't need to care about other macros.

lilactown19:10:39

you don’t even need a macro if you use tag literals 🙂

lilactown19:10:05

the reader can output equivalent react/createElement calls

juhoteperi19:10:18

Aha, yes, interesting.

lilactown19:10:28

the other issue that comes up is dynamic props, which can also potentially be solved with some additional syntax

lilactown19:10:55

ex. [:div x y] is ambiguous whether x is props or a child

lilactown19:10:22

I’ve put most of this in an experimental lib if you are curious: https://github.com/Lokeh/thump/

lilactown19:10:59

it has some bugs… someone told me they couldn’t get it to work in their app a little while ago so YMMV. but it has the general thoughts written out in the README at least 😅

pesterhazy19:10:21

The reader literal idea is nice

thheller19:10:59

the reader literal idea is horrible 😛

😭 4
lilactown19:10:27

thought you might chime in thheller. we shall see!

lilactown19:10:15

hiccup is fundamentally syntax, not data. it makes sense to treat it as such IMO

thheller19:10:52

#h/e [:div "foo"] vs (h/e [:div "foo"]) (with h/e being a macro)

thheller19:10:54

don't know why you'd chose a data literal for what macros can do better 😛

lilactown19:10:19

IMO the additional parens reduces readability, but that might be subjective

lilactown19:10:45

I think people have a pretty regular reaction when you get to the cases where you need to nest: "ugh, too many parens"

thheller19:10:15

to me the syntax just looks "off" with literals

p-himik19:10:55

As far as I can tell: Literals: Pros: - 1 symbol less Cons: - Impossible to compose - Impossible to reuse directly without knowing how they're implemented - Impossible to thread with -> and the like Marcos: All opposite.

lilactown19:10:29

I'm not sure what you mean by composition / reusing

lilactown19:10:41

I don't really get the threading con either

lilactown19:10:43

And it's not just 1 less symbol. The effect is 1 level less visual nesting, which I find significant when I read code

lilactown19:10:49

Again, subjective

p-himik19:10:20

With macros, I can write (a (b (c ...))). With reader literals, I can go take a look at how it's implemented and then maybe somehow achieve the same. My argument is completely invalid if it's possible to implement a reader literal in such a way so that it's possible to use it as #h/e (into [:div] ...) or something like that. I don't understand the argument about visual nesting - the data has the exact same left boundary in terms of characters.

lilactown19:10:21

You wouldn't write code like that

lilactown19:10:15

My general hypothesis is "hiccup is syntax, not data"

thheller19:10:56

when is syntax yet you don't see us writing #when 😉

thheller19:10:45

hiccup is data as it is. you are only doing the literals because of performance

lilactown19:10:53

It should be written literally. 90% of the cases when I see people doing operations like into/concat/etc. It's unreadable and usually do to some hindrance in the hiccup API irself

lilactown19:10:46

The nice thing about the approach I'm taking with thump is that it exposes a reader tag, macro, and runtime interpreter

p-himik19:10:01

IMO hiccup is 100% data. Especially when we have freedom to define components that can be used in any form other than [component arguments? children*] (e.g. [component arg1 arg2 arg3 children]). I do lots and lots of data processing with hiccup before sending it to Reagent.

lilactown19:10:26

So for the cases when you want some more dynamism, you can fall back to macro or runtime. But it's explicit and will stand out

thheller19:10:41

the bad part is the compilation aspect. you can use #h/e technically without a :require. if you do the compiler cannot ensure that the code supporting #h/e is actually "ready"

thheller19:10:02

so technically you are forced to add the :require just to avoid weird compilation results

thheller19:10:55

well you can do whatever you like. I feel like we had this discussion before 🙂

lilactown19:10:22

This is avoided with thump by requiring the user to include a hiccup-element var which should be provided by a ns that requires the core tag literal ns

lilactown19:10:56

I know 🎃 I like discussing it with you and everyone. It helps me figure things out