hexops / vecty

Vecty lets you build responsive and dynamic web frontends in Go using WebAssembly, competing with modern web frameworks like React & VueJS.
BSD 3-Clause "New" or "Revised" License
2.82k stars 144 forks source link

How to update a rendered element? #304

Closed Naatan closed 2 years ago

Naatan commented 2 years ago

Due to lack of documentation I'm working off of examples, issues and backwards engineering. Sadly I feel like the issue of updating already rendered elements isn't easily discerned through any of those channels.

From the example code I found that I probably want to use vecty.Rerender(elem). But so far any attempt I give this results in the following error:

panic: vecty: next child render must not equal previous child render (did the child Render illegally return a stored render variable?)

I find this error hard to understand. Is it telling me that once rendered an item cannot be rendered again? And if so, how am I meant to update rendered elements? I'm guessing there's a detail I'm missing.

The todo example doesn't seem to be doing anything fancy, I'm not sure what I'm doing wrong.

For what it's worth, here's an excerpt of the code I'm working with:

type listitem struct {
    vecty.Core
    selected      bool
}

func (c *listitem) onClick(e *vecty.Event) {
    c.selected = true
    vecty.Rerender(c)
}

func (c *listitem) Render() vecty.ComponentOrHTML {
    return elem.Div(
        vecty.Markup(
            vecty.MarkupIf(c.selected, vecty.Class("selected")),
            event.Click(c.onClick),
        ),
    )
}
slimsag commented 2 years ago

Make sure that your Render method is always responsible for creating new components/elements. Probably what you are doing is storing listitem in a list somewhere and returning that multiple times in your Render function. You need to return new listitems every time Render is called.

Naatan commented 2 years ago

Thanks, that was also the direction I was trying to move in. I had changed my component to instead of receiving a slice of MarkupOrChild instead use a callback to receive that same slice. No dice so far though.. still trying to find out what I'm doing wrong.

What's the "vecty" way of creating a component that accepts child elements / components?

Naatan commented 2 years ago

Turns out I missed a component. After changing my components to look like this:

type listitem struct {
    vecty.Core
    selected      bool
    markupOrChild func() []vecty.MarkupOrChild
}

It now works. But the markup is getting super unwieldy. Is this the recommended way? It does not feel intuitive.. sadly the intuitive approach is evidently error prone.

Naatan commented 2 years ago

Also have to wonder what this does for previously initialized structs. eg. if I set c.selected=true and it gets rerendered then either the property change is lost or vecty is doing something really questionable. Appreciate any insights into how the process works.

VinceJnz commented 2 years ago

I struggled with this when I was trying to get it to work.

Have you looked at the todomvc example.

https://github.com/hexops/vecty/blob/41ffb63fbae14f152f449cddd0bd393764683562/example/todomvc/components/pageview.go#L171-L179

This renders a list of store.Items Each time through the for loop it retrieves an item from store.Items, it populates a new ItemView with the item data and appends it to items (vecty.List).

properties vs. state Structure fields can be either a property or a state If they are to be a property then you need to add `vecty:"prop"` e.g.

type ItemView struct {
    vecty.Core
    SomePropField `vecty:"prop"`
    SomeStateField
}

When an ItemView is rerendered:

Naatan commented 2 years ago

I see. So basically Vecty wants you to completely separate the rendering code from the state management code? I guess that makes sense, and is likely somewhere where most devs would end up anyway after the prototyping phase. It does still feel quite error prone though.

Appreciate the insights! I'm at least unblocked for now. For what it's worth I'd consider this the only part of vecty so far that I'd like to see some improvement on, everything else has been great! And to be fair it's not that this part is "bad", it's just not intuitive and prone to error.

Naatan commented 2 years ago

By the way, can anyone tell me what this panic achieves? It seems disabling the panic makes the code function exactly the way I had assumed it would. Though I'm guessing it will create some type of breakage in other areas that I haven't reached?

VinceJnz commented 2 years ago

It might be worth having a read of the following discussion

https://github.com/hexops/vecty/issues/291

Naatan commented 2 years ago

@VinceJnz Thanks, that's a useful read. Is implied in your response that the mechanic that this panic guards for is specifically this prop vs state mechanic?

It feels like a steep price to pay, surely there's better ways of addressing this use-case.

Naatan commented 2 years ago

fwiw I ended up working around this issue with the following helper:


type MarkupOrChildProxy struct {
    vecty.Core
    Child vecty.MarkupOrChild
}

func RenderMarkupOrChild(markupOrChild ...vecty.MarkupOrChild) []vecty.MarkupOrChild {
    result := []vecty.MarkupOrChild{}
    for _, m := range markupOrChild {
        switch v := m.(type) {
        case vecty.MarkupList:
            result = append(result, v)
        case vecty.Component, *vecty.HTML, vecty.List, vecty.KeyedList:
            result = append(result, &MarkupOrChildProxy{Child: v})
        default:
            panic(fmt.Sprintf("unsupported markupOrChild type: %T", v))
        }
    }
    return result
}

func (c *MarkupOrChildProxy) Render() vecty.ComponentOrHTML {
    return c.Child.(vecty.ComponentOrHTML)
}

func (c *MarkupOrChildProxy) SkipRender(prev vecty.Component) bool {
    switch prev.(type) {
    case *MarkupOrChildProxy:
        return true
    }
    return false
}

This was influenced by other vecty projects I found that were using similar helpers.

Hopefully this type of workaround won't be required once vecty goes stable.