Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

question: What is this prop vs. state idea #291

Open
soypat opened this issue Jan 12, 2022 · 3 comments
Open

question: What is this prop vs. state idea #291

soypat opened this issue Jan 12, 2022 · 3 comments
Labels

Comments

@soypat
Copy link
Collaborator

soypat commented Jan 12, 2022

I've noticed component structs sometimes have fields with vecty:"prop" tag. I've also come across the issue #209 and the generics issue #277.

What is this separation?Why is it necessary? I've built vecty apps without it just fine.

@VinceJnz
Copy link

VinceJnz commented Jan 13, 2022

These are the notes that I have found and added to on state vs. property

vecty:"prop"

Related issue: #209

What does the 'vecty:"prop"' struct tag do?
Any variable tagged with 'vecty:"prop"' will have its value over-written when it is updated by the parent. Other variables are called state values.
The way this is implemented in vecty: The old copy of the struct is kept and the prop variables are over-written with the prop variables from the new struct. The new struct is discarded. So the old copy becomes the new master version with the prop variables over written, and the state variables are kept unchanged.

This allows:

  1. State values such as an incrementing counter to be kept
  2. The parent object to set the child properties

If the value of a variable is set by the parent and is not changed within the methods of the current struct then is should be set as a prop variable (i.e. is will have the 'vecty:"prop"' tag)
If the value of a variable is set in a function in the struct then it should be set as a state variable (i.e. no tag)

These rules apply to all type of variables and include types: struct, func, pointer, interface, etc.

Warning Notes to self

To be able to use a pointer to the correct version of state variables you will need to make the pointer from the first instance of the structure. This applies to func, pointers, interfaces. This is because the old (original) structure is the master for all state variables. This is not recommended.
This has a big impact when you are rendering elements: e.g. a new child is created when rendering the parent. The original child is kept along with state variables and prop variable are over written.
Getting this wrong can cause errors like: panic: vecty: Rerender invoked on Component that has never been rendered because you are trying to render the new version of the struct.

More notes on panic: vecty: Rerender invoked on Component that has never been rendered
It is important to seperate the store/data fetching from the view rendering.
For example do not include the data fetch functions into the view. Have the data read done outside the view and then call the view rerender.

Regarding using public vs. private fields

I think that can be confusing because you are then applying semantic meaning to public vs. private as being "public to the component" and "private to the component" rather than to the package. Effectively, you would be replacing 'vecty:"prop"' with whether or not a field is public or not. This could be very confusing when working in e.g. one Go package
this said another way, if you made a counter field public on a component, its value would no longer persist across renders which would be mighty confusing I think.

What matters to me the most is the mental model of the Vecty API. Coming from a React background, I just always expect the parent to be the ruler of the most updated values to a given prop, regardless of how many times it rerenders. If a child wants to keep its own state, then it just copies the value to another field during Mount time. I agree about the private/public comment you made.

@slimsag
Copy link
Member

slimsag commented Jan 13, 2022

This gets asked a lot, what @VinceJnz said seems all accurate though, but actually there's a very simple answer.

  • Does the parent component own choosing the value of the field? Will the parent want to change that value when it it re-rendered later on?
    • If yes, then it should be a vecty:"prop"
    • If no, then it's not a property, it's state.

Example: When to use vecty:"prop"

Pretend you have a AlarmClock component, and you have a goroutine which calls vecty.Rerender(alarmClock) every second to update it. Let's say the Render method of AlarmClock returns some HTML like The current time is: and then renders a child component &CurrentTime{Time: alarmClock.time}

If the CurrentTime.Time field is tagged as vecty:"prop" then every time the parent AlarmClock is rerendered, the value it passes into &CurrentTime{Time: alarmClock.time} is going to end up being used, e.g. it'll update like you would expect an alarm clock to!

But if it's not tagged with vecty:"prop"? Then the first time the parent renders &CurrentTime{Time: alarmClock.time} the Time field gets set to e.g. a value of 1, but even when it gets rerendered with a new value 2 the CurrentTime component will actually just continue using the original (first-render) value of 1! The alarm clock doesn't update!

Example: When NOT to use vecty:"prop"

Let's say you have a GameWorld component which renders &Player{Health: 100} so the player starts out with 100 health points. In response to input, you call a method on your Player component player.takeDamage() which sets player.Health -= 1 and then calls vecty.Rerender(player) so it shows the new health value.

If Health is tagged with vecty:"prop" then you'll see some weird behavior here! Any time vecty.Rerender(gameWorld) is used - say to add more monsters to your game world - then suddenly your player's health gets reset back to 100! What gives?! It's because Health being a vecty:"prop" is saying that the parent chooses the value of the field, and so if our parent gets rerendered.. it's choosing the value!

About public vs. private fields

A vecty:"prop" has to be a public field, state can be public or private fields (but is advised to be private.)

This is simply because Go reflection cannot access private fields, which is how vecty:"prop" is implemented behind the scenes.

@soypat
Copy link
Collaborator Author

soypat commented Jan 13, 2022

Thanks for the explanation @slimsag! I just noticed I was doing some hacky workarounds to get things that should have been props to render, like calling a struct's render method instead of embedding it!

All in all I think I've understood when to use one or the other. Here's an example for whomever wants to play around with slimsags Alarm Clock idea, it really helped drive the idea home. Click on the time to cause a rerender on the parent's (AlarmClock) side.

Click to see code example
package main

import (
	"time"

	"github.com/hexops/vecty"
	"github.com/hexops/vecty/elem"
	"github.com/hexops/vecty/event"
)

func main() {
	vecty.SetTitle("Hello Vecty!")
	vecty.RenderBody(&PageView{})
}

// PageView is our main page component.
type PageView struct {
	vecty.Core
}

// Render implements the vecty.Component interface.
func (p *PageView) Render() vecty.ComponentOrHTML {
	return elem.Body(
		vecty.Text("Hello Vecty!"),
		elem.Paragraph(&AlarmClock{}),
	)
}

type AlarmClock struct {
	vecty.Core
}

func (p *AlarmClock) Render() vecty.ComponentOrHTML {
	t := &CurrentTime{
		Time: time.Now(),
	}
	return elem.Div(
		vecty.Markup(event.Click(func(e *vecty.Event) {
			vecty.Rerender(p)
		})),
		t, // Once can also just call t.Render() and use the result to not have to use `props` (a.k.a BAD PRACTICE)
	)
}

type CurrentTime struct {
	vecty.Core
	Time time.Time `vecty:"prop"`
}

func (p *CurrentTime) Render() vecty.ComponentOrHTML {
	return elem.Span(
		vecty.Text(p.Time.String()),
	)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants