Closed mar1n3r0 closed 2 years ago
Hmmm seems like the approach is the same as the UI goroutine. Does that mean that if I create an external state management store package all it needs in order to be compatible is to make it a dependency that the framework opens channel x for communication in between ? Or could the store open this channel when imported to the framework and call the UI goroutine on state changes ?
Let's say we have this state managament store:
https://github.com/luisvinicius167/godux
What would it take for go-app to work with it as an alternative to the centralized HTTP requests and caching so that each component can set and get shared state from the store instead ?
The way I understand it so far the godux package needs to call Dispatch and Update on the UI goroutine everyime a state is updated. This seems to be an issue when it comes to compatibility with other frameworks since each of them would name the goroutine in different ways or maybe it can scan for running goroutines and find the rendering one by some criteria if there are more than one...
The ultimate goal I see from my usage so far is to have a fully automated re-rendering of single components selectively based on changed state. That is to say if a global state property is changed and components a,b and c depend on it they immediately re-render but not component d which does not depend on that property. And the hardest part is to get that right when multiple components update it async without centralized HTTP requests and multiple calls to get a cached version of the state.
There is no observable in Go and doing a mechanism like this results in a very heavy API that no one would enjoy using.
I had been able to nearly suppress the need for manual updates in v9 which really improve the usability of the package. => https://github.com/maxence-charriere/go-app/issues/519
This was made possible because I know now where things are updated. I also introduced a Mediator mechanism that allows components to subscribe to some messages. That can be used to propagate a state across multiple components without them having dependencies between each other.
type Context interface {
context.Context
// Setups the handler to listen to the given message.
Handle(msg string, h MsgHandler)
// Dispatches the given value to all the handlers that listen for the given
// message.
Post(msg string, v interface{})
// ...
}
// MsgHandler represents a handler to listen to messages sent with Context.Post.
type MsgHandler func(Context, interface{})
You could pass a context to another tool and use the Post
function to propagate a given state.
Thanks I was looking for the observable pattern indeed. I do appreciate all the efforts but I still believe that an external add-on store is the most elegant solution to that and allows observance. Plus it allows legacy versions to get it separately. Horizontal communication would easily become unmanageable at scale since it's a message broker pattern in essence which can also be an external add-on rather than being built-in. Including a message broker makes quite a vendor lock in as well. I believe at scale everything should be as modular as possible otherwise upgrades between versions for bigger projects are a dead end.
It seems it is not only possible but pretty much standardized:
https://gobyexample.com/stateful-goroutines
Anytime a component wants to read data it sends the corresponding key to the read chan on the state goroutine. Anytime it wants to write data it sends key and val to the write chan on the state goroutine. In that case component structs are it's internal props only. I will try and make it a separate package and test with one of the demos.
It shouldn't require any changes to the API itself and can remain an optional separate package that can work with all other Go wasm frameworks too the same way thus making it a universal state store.
I was working on https://murlok.io and I finally get to a point where I needed state as you describe it.
After reading about redux and the godux implementation, I tried different approaches to provide something similar. It ended with more or less complicated things that I did not like.
Eventually, I realized that the redux/godux really seems like a mediator pattern where msg
is called action
, coupled with a GetState/SetState that is actually a concurrent safe key/value store.
I'm still experimenting but so far those are the requirements I see in order to decouple a state from components. | Requirement | Redux/Godux | go-app |
---|---|---|---|
Propagate an action | store.Dispatch(actionType Action) godux.Action( Type string, Value interface{}) |
ctx.Post(action string, v interface{}) |
|
Handle an action | store.Reducer(func(action godux.Action)) |
app.Handle(action string, func (ctx app.Context, v interface{})) - aynchronous |
|
Notify the UI (component) | store.Reducer(func(action godux.Action)) ? |
ctx.Handle(action string, func (ctx app.Context, v interface{})) - on UI goroutine |
|
Set a state | godux.SetState(name string, value interface{}) |
ctx.States().Set(name string, v interface{}) |
|
Get a state | godux.GetState(name string) interface{} |
ctx.States().Get(name string) v interface{} |
Why finally I'd like to implement something like this?
Context
allows me to keep track of mounted components. It allows to automatically remove action handlers without the user bothering to do it when the component is dismounted, which gives a better APINote that I'm still experimenting with this and there are some problems I may not grasp yet. Thanks for debating about this, I feel it really helps to have a better API with this package.
Thanks for taking the time to elaborate on the details. I also believe the mediator pattern is very useful for component communication. The store and the mediator are different non-related concepts though. Coming from a few years of vue experience and js ecosystem bloatware disappointment I am very frightened that the Go wasm ecosystem will become very opinionated and non-compatible with each other unless it is taken into consideration in it's early stages. This is why I consider mediators and state stores to be separate packages and most of all working with other projects like vugu and vecty at the same time. Including an opinionated solution to something in the core package makes it a vendor lock-in forever.
The store is way simpler than redux due to stateful routines. All it needs is open channels for read and write. I am working on figuring them out in a separate package so that the framework can just use them.
@maxence-charriere Here is a very basic in-memory key value store based on stateful goroutines.
https://github.com/mar1n3r0/gostatestore
The scenario I am testing is the hello demo package, creating a second component and trying to get and set the same shared property. The issues I have so far is that if each component is a new tab they don't seem to share memory at all - are they 2 completely isolated instances ?
The other one which I am keen on trying is to have a single page composed of two components and observing updates between them. No idea how to do that yet with 2 separate structs passed for the same path and composing UIs.
I think something of use could be a hooks API per component which can be called from any external package to perform actions that are otherwise internal like render() otherwise I don't see how the static nature can be changed towards a state managed behavior.
are they 2 completely isolated instances?
Memory is not shared between browser tabs. You have to use things like local storage to do this kind of thing. This is browser behavior.
are they 2 completely isolated instances?
Memory is not shared between browser tabs. You have to use things like local storage to do this kind of thing. This is browser behavior.
Forgot about it. This in theory means that... the only possible state store for Web Assembly is a key-value store that is running on a separate server with its own address. Something like memcached etc.
How about the SPA approach? Say we wanna render two components in the same single page app and at the same address. How do we go about rendering the two markups in one place ?
Forgot about it. This in theory means that... the only possible state store for Web Assembly is a key-value store that is running on a separate server with its own address.
You can implement a key-value store based on local storage or indexedDB. This is not a WebAssembly specificity.
How about the SPA approach? Say we wanna render two components in the same single page app and at the same address. How do we go about rendering the two markups in one place ?
This is impossible that they have the same memory address. You can have 2 components of the same type on your page but that will not be the same instance.
https://github.com/AOHUA/redux-state-sync#readme
I thought something like this would be possible with Go channels.
You can probably do a store based on this. You will have to write it using https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel.
I just don't understand why you want to use channels.
you could do something like:
type Store interface{
// Set the state and trigger updte of values registered with observe.
Set(ctx context.Context, state string, v interface{})
// Get a given state.
Get(ctx context.Context, state string) interface{}
// Would register the given value to be updated when a state is set. I would work like json.Unmarshal.
Observe(ctx context.Context, state string, v interface{})
}
You would have to implement the internal logic but there is no need for channel. Just sync.Mutex
I updated the previous msg
It it because I mistakenly thought that go channels can be used on the web which was a wrong assumption and makes real-time state management impossible with Go only. Even if I implement broadcasting channels that would be basically using syscall/js.
The ultimate goal was to have global state available anywhere in real-time. So no matter which tab and component you are in you listen to a channel to get the current state.
Channels are usable on the Web. I use it for some stuff in go-app.
Channels are usable on the Web. I use it for some stuff in go-app.
I had an in-depth look at the ui goroutine but noticed that it is working only until the wasm is compiled after that it's all web workers and no Go.
The current basic package gostatestore manages to maintain state within the component via channels but no way to pass it to other components.
I had an in-depth look at the ui goroutine but noticed that it is working only until the wasm is compiled after that it's all web workers and no Go.
This is incorrect. Besides the wasm loader and the pwa requirement, All is pure Go.
You can probably do a store based on this. You will have to write it using https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel.
I just don't understand why you want to use channels.
you could do something like:
type Store interface{ // Set the state and trigger updte of values registered with observe. Set(ctx context.Context, state string, v interface{}) // Get a given state. Get(ctx context.Context, state string) interface{} // Would register the given value to be updated when a state is set. I would work like json.Unmarshal. Observe(ctx context.Context, state string, v interface{}) }
You would have to implement the internal logic but there is no need for channel. Just
sync.Mutex
Mutexes indeed are an alternative but given that we have channels I just thought it's worth to use it to our advantage.
See differences here:
https://gobyexample.com/mutexes
https://gobyexample.com/stateful-goroutines
In the previous example we used explicit locking with mutexes to synchronize access to shared state across multiple goroutines. Another option is to use the built-in synchronization features of goroutines and channels to achieve the same result. This channel-based approach aligns with Go’s ideas of sharing memory by communicating and having each piece of data owned by exactly 1 goroutine.
--
Mutexes indeed are an alternative but given that we have channels I just thought it's worth to use it to our advantage.
I recommend you this article: https://github.com/golang/go/wiki/MutexOrChannel
Unfortunately both approaches don't solve the issue that Go channels are not web channels and the browser does not understand them as BroadcastChannel type.
https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API
I think this could work. Still experimenting with how to pass a callback to event listener but it looks like the API is not dispatching.
When a message is posted, a message event is dispatched to each BroadcastChannel object connected to this channel. A function can be run for this event with the onmessage event handler:
func (h *hello) OnMount() {
bc := js.Global().Get("BroadcastChannel").New("test1")
bc.Call("postMessage", "This is a test message.")
go func() {
bc.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
message := args[0].String()
fmt.Println("message received ")
fmt.Println(message)
return message
}))
app.Dispatch(func() {
h.Update()
})
}()
}
https://www.digitalocean.com/community/tutorials/js-broadcastchannel-api
Things You Can Do with Broadcast Channels There are many things we can imagine. The most obvious use case is to share states. For example, if you use something like Flux or Redux to manage your state, you can broadcast a message so that your store remains the same across tabs. We can also imagine building something similar for state machines.
That actually worked in the end.
func (h *hello) OnMount() {
bc := js.Global().Get("BroadcastChannel").New("test1")
bc.Call("postMessage", "This is a test message.")
}
func (h *welcome) OnMount() {
bc := js.Global().Get("BroadcastChannel").New("test1")
bc.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
message := args[0].Get("data").String()
fmt.Println("message received ")
fmt.Println(message)
return message
}))
}
And here is a full demo of cross-component reactivity and cross-tabs reactivity using BroadcastChannel API only. No need for components passing references to each other.
hello.go
package main
import (
"fmt"
"syscall/js"
"github.com/maxence-charriere/go-app/v6/pkg/app"
)
type hello struct {
app.Compo
name string
Name string
bc js.Value
}
func (h *hello) OnMount() {
h.bc = js.Global().Get("BroadcastChannel").New("test1")
h.bc.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
message := args[0].Get("data").String()
fmt.Println("message received ")
fmt.Println(message)
h.Name = message
h.Update()
return message
}))
}
func (h *hello) Render() app.UI {
return app.Div().Body(
app.Main().Body(
app.H1().Body(
app.Text("Hello, "),
app.If(h.Name != "",
app.Text(h.Name),
).Else(
app.Text("World"),
),
),
app.Input().
Value(h.name).
Placeholder("What is your name?").
AutoFocus(true).
OnChange(h.OnInputChange),
app.A().
Class("app-button section").
Href("welcome").
Body(
app.Text("Welcome"),
),
),
)
}
func (h *hello) OnInputChange(src app.Value, e app.Event) {
h.Name = src.Get("value").String()
app.Dispatch(func() {
h.bc.Call("postMessage", h.Name)
h.Update()
})
}
welcome.go
package main
import (
"fmt"
"syscall/js"
"github.com/maxence-charriere/go-app/v6/pkg/app"
)
type welcome struct {
app.Compo
name string
Name string
bc js.Value
}
func (h *welcome) OnMount() {
h.bc = js.Global().Get("BroadcastChannel").New("test1")
h.bc.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
message := args[0].Get("data").String()
fmt.Println("message received ")
fmt.Println(message)
h.Name = message
h.Update()
return message
}))
}
func (h *welcome) Render() app.UI {
return app.Div().Body(
app.Main().Body(
app.H1().Body(
app.Text("Welcome, "),
app.If(h.Name != "",
app.Text(h.Name),
).Else(
app.Text("World"),
),
),
app.Input().
Value(h.Name).
Placeholder("What is your name?").
AutoFocus(true).
OnChange(h.OnInputChange),
app.A().
Class("app-button section").
Href("/").
Body(
app.Text("Hello"),
),
),
)
}
func (h *welcome) OnInputChange(src app.Value, e app.Event) {
h.Name = src.Get("value").String()
app.Dispatch(func() {
h.bc.Call("postMessage", h.Name)
h.Update()
})
}
cool 👍 .
Just a note:
func (h *welcome) OnInputChange(src app.Value, e app.Event) {
h.Name = src.Get("value").String()
app.Dispatch(func() {. // <-- here you don't need to call dispatch. go-app event handler are dispatched by default.
h.bc.Call("postMessage", h.Name)
h.Update()
})
}
Hey @mar1n3r0, I'm currently working on a store implementation integrated into go-app. I found what you show me pretty interesting but since it is not supported on Safari, I'm still undecided about whether I should implement it.
What would be the use-cases where there is a need for a cross tab/window state?
What would be the use-cases where there is a need for a cross tab/window state?
I think this would be a very useful feature... just my 2 cents
Hey @mar1n3r0, I'm currently working on a store implementation integrated into go-app. I found what you show me pretty interesting but since it is not supported on Safari, I'm still undecided about whether I should implement it.
What would be the use-cases where there is a need for a cross tab/window state?
Given that it's widely supported among all other browsers it will land in Safari at some point. Besides the cases @c-nv-s mentioned for me this feature is a non-opinionated easy to implement global state.
The way Go treats wasm as an application means it runs and exits. This is quite limiting compared to modern JS frameworks with their reactive APIs which allow real-time reactivity on external changes. Is this even possible with the current state of things? Maybe keeping dedicated channels open infinite? This would allow all kind of things from state management stores to webhooks to websockets etc.
https://www.aaron-powell.com/posts/2019-02-06-golang-wasm-3-interacting-with-js-from-go/
Here is an example:
I would like to connect the app to an external state management store and make it re-render on state changes. By the time the state changed the runtime has exited, hence unless there is a channel open to keep it alive it can't react to external events after exit.