Dinamically update component?
Hello, i just started to use this go-app tool.
Basically my problem is that i tried to build a component that dynamically changes it's contents with the data received from an http request. But this looks like it's a dumb approach because it only seems to update the contents when i restart the server.
Maybe someone can tell me how could this be done or where to look for a solution to what i'm trying to accomplish.
Thanks for all!
May I ask when you expect the changed content to be retrieved?
I was under the assumption that the component function would get called every time the component needed to be rendered. That is what i was trying to accomplish. This doesn't seem to work this way, and because of this, the data is only retrieved at the startup of the server.
I think my problem is that i have a fundamental misunderstanding on how this all works, If you could share some insight on how could this be done i'd be grateful.
Basically what i want to get is this:
- User hits refresh page
- Server make http request to get updated data
- Place that data into the component structure.
- Render component to the page.
Your point 2 does not make sense to me. Why would the "server" request updated data? When the user presses refresh. the browser will refresh the page by loading it (again) from the server. In the case of go-app and you are not using "OnPreRender()" the server will not do anything regarding your components. They are handled in the front end (by "the client").
To actually have different data on the same "URL" (without recompiling your program) you need to get the data "in the client" and then "dynamically render it".
One usually does not want the user to hit refresh for that (actually this is the last thing I want my users to do when I implement my own routing in the front end).
I think you should read:
- https://go-app.dev/architecture
- https://go-app.dev/seo
- https://go-app.dev/components
You could also try to read and understand: https://github.com/maxence-charriere/go-app/tree/42eb629a51fb842c0da7abfa7e65b5bd1956ed1c/docs/src which loads most of its data as markdown files from the client and converts it to HTML to render the pages.
BTW: From what you wrote so far I am not sure if you actually "need" go-app (a PWA framework) but maybe should look at one of the normal HTML server frameworks (there are plenty).
@yosoymau maybe be a code snippet of why you try to do would help understand where is the struggle.
Thank you for your answers! I've now come to the realization that i don't really have a clue of what it is that i am doing and that i should read more about front end development because i am starting to understand how my questions are, as you've said, kinda non sensical lol, because of this i think we should close this issue.
Sorry for the inconvenience and thanks a lot for your answers, they've been very useful to shine some light into my clueless mind lol.
I just try to implement a simple clock, how do I make the time reflected on GUI every seconds?
func (c *Clock) OnMount(ctx app.Context, a app.Action) {
for range time.Tick(time.Second) {
c.Time = time.Now().Format(time.RFC3339)
c.Update()
}
}
c.Time
make that lowercase (c.time) and try again. You may also not use c.Update() but call ctx.Dispatch(func(ctx app.Context) { c.Time = time.Now().Format(time.RFC3339) })
func (c *Clock) OnMount(ctx app.Context, a app.Action) looks wrong too (it does not sound as this is an action handler). I suggest you add var _ app.Mounter = (*Clock)(nil) to validate your implementation fits the interface.
And of course, you need to make that timer loop asynchronously:
package clock
import (
"github.com/maxence-charriere/go-app/v9/pkg/app"
"time"
)
type Clock struct {
app.Compo
time string
}
var _ app.Mounter = (*Clock)(nil)
func (c *Clock) OnMount(ctx app.Context) {
ctx.Async(func() {
for range time.Tick(time.Second) {
ctx.Dispatch(func(ctx app.Context) {
c.time = time.Now().Format(time.RFC3339)
})
}
})
}
func (c *Clock) Render() app.UI {
return app.Div().Class("clock").Text(c.time)
}
I was curious how to make it useable for real:
package clock
import (
"github.com/maxence-charriere/go-app/v9/pkg/app"
"github.com/metatexx/go-app-pkgs/dbg"
"time"
)
type Clock struct {
app.Compo
Format string
Class string
time string
running bool
}
var _ app.Mounter = (*Clock)(nil)
var _ app.Dismounter = (*Clock)(nil)
func (c *Clock) OnMount(ctx app.Context) {
ctx.Async(func() {
c.running = true
for range time.Tick(time.Second) {
if !c.running {
dbg.Log("stopped")
break
}
ctx.Dispatch(func(ctx app.Context) {
c.updateTime()
})
}
dbg.Log("exits")
})
c.updateTime()
}
func (c *Clock) updateTime() {
c.time = time.Now().Format(c.Format)
}
func (c *Clock) Render() app.UI {
return app.Div().Class(c.Class).Text(c.time)
}
func (c *Clock) OnDismount() {
dbg.Log("stopping")
c.running = false
}
usage like&clock.Clock{Format: "15:04:05 02.01.2006", Class: "text-gray-300"}. German Date/Time + TailwindCSS in that example.
Also curious, slightly different implementation for timer cleanup
Edit: Updated with some changes to make sure the for loop exits on dismount
package clock
import (
"time"
"github.com/maxence-charriere/go-app/v9/pkg/app"
)
type Clock struct {
app.Compo
time string
done chan bool
ticker *time.Ticker
}
var _ app.Mounter = (*Clock)(nil)
var _ app.Dismounter = (*Clock)(nil)
func (c *Clock) OnMount(ctx app.Context) {
c.done = make(chan bool, 1)
c.ticker = time.NewTicker(time.Second * 1)
c.updateTime(ctx)
ctx.Async(func() { c.timerLoop(ctx) })
}
func (c *Clock) timerLoop(ctx app.Context) {
for {
select {
case <-c.ticker.C:
ctx.Dispatch(c.updateTime)
case <-c.done:
c.ticker.Stop()
return
}
}
}
func (c *Clock) updateTime(_ app.Context) {
c.time = time.Now().Format(time.RFC3339)
}
func (c *Clock) Render() app.UI {
return app.Div().Class("clock").Text(c.time)
}
func (c *Clock) OnDismount() {
c.done <- true
}
@mlctrez It seems like your timer code is better as my one still leaks the timer.
But the problem with the way @mlctrez stops the timer (for me) is, that it does not seem to run the part of the function after the for{} so I do not get to see the dbg.Log("exits") from my example if I use his code. I do not understand why that is the case? I even changed the dbg.Log("exits") with creating a panic, because I thought I might just not get output.
I ended up with:
package clock
import (
"github.com/maxence-charriere/go-app/v9/pkg/app"
"github.com/metatexx/go-app-pkgs/dbg"
"time"
)
type Clock struct {
app.Compo
Format string
Class string
time string
running bool
ticker *time.Ticker
}
var _ app.Mounter = (*Clock)(nil)
var _ app.Dismounter = (*Clock)(nil)
func (c *Clock) OnMount(ctx app.Context) {
ctx.Async(func() {
c.ticker = time.NewTicker(time.Second * 1)
defer func() {
c.ticker.Stop()
dbg.Log("ticker stopped")
}()
c.running = true
for range c.ticker.C {
if !c.running {
dbg.Log("stopped")
break
}
ctx.Dispatch(func(ctx app.Context) {
c.updateTime()
})
}
dbg.Log("exits")
})
c.updateTime()
}
func (c *Clock) updateTime() {
c.time = time.Now().Format(c.Format)
}
func (c *Clock) Render() app.UI {
return app.Div().Class(c.Class).Text(c.time)
}
func (c *Clock) OnDismount() {
dbg.Log("stopping")
c.running = false
c.ticker.Reset(1)
}
This results in:
pkg/components/clock.(*Clock).OnDismount: stopping
wasm_exec.js:349 pkg/components/sortabletable.(*SortableTable).OnDismount
wasm_exec.js:349 pkg/components/clock.(*Clock).OnMount.func1: stopped
wasm_exec.js:349 pkg/components/clock.(*Clock).OnMount.func1: exits
wasm_exec.js:349 pkg/components/clock.(*Clock).OnMount.func1.1: ticker stopped
This code also renders the clock as soon as possible (and not just after the first tick).
Edit: The c.ticker.Reset(1) was added later and makes it stop immediately (before that it stopped only after the next tick).
@oderwat I totally missed that time.Ticker.Stop() does not close the channel. That's why the for loop never exits.
@oderwat I totally missed that time.Ticker.Stop() does not close the channel. That's why the for loop never exits.
Oh, yes, so it just stops, and the loop freezes. Makes sense.
@oderwat @mlctrez really cool and exactly what I what. many many thanks
Just started to use go-app .. i am kinda in love but to the point. something lite and dirty like this will work? It will benefit if you have an interface TaskManager in the app
package tasks
import (
"sync"
"time"
"github.com/google/uuid"
"github.com/maxence-charriere/go-app/v9/pkg/app"
)
var ImmediateTask time.Duration = 0 * time.Second
type Task struct {
ID uuid.UUID
Name string
ctx app.Context
every time.Duration
ticker *time.Ticker
mux sync.Mutex
cFunc func(ctx app.Context)
running int
}
func NewTask(ctx app.Context, name string, every time.Duration, clientfunc func(ctx app.Context)) *Task {
tsk := new(Task)
tsk.Name = name
tsk.ctx = ctx
tsk.running = 0
tsk.ID = uuid.New()
tsk.every = every
if tsk.every > 0 {
tsk.ticker = time.NewTicker(tsk.every)
}
tsk.cFunc = clientfunc
return tsk
}
func (t *Task) Run() {
t.ctx.Async(func() {
t.running = 1
defer func() {
t.ticker.Stop()
app.Log("ticker stopped")
}()
if t.every > 0 && t.ticker != nil {
for range t.ticker.C {
if t.running == 0 {
app.Log("stopped")
break
}
t.ctx.Dispatch(func(ctx app.Context) {
t.mux.Lock()
defer t.mux.Unlock()
t.cFunc(ctx)
})
}
} else {
t.ctx.Dispatch(func(ctx app.Context) {
t.mux.Lock()
defer t.mux.Unlock()
t.cFunc(ctx)
})
}
app.Log("exits")
})
if t.every > 0 {
t.mux.Lock()
defer t.mux.Unlock()
t.cFunc(t.ctx)
}
}
func (t *Task) Unlock() {
t.mux.Lock()
}
func (t *Task) Lock() {
t.mux.Lock()
}
func (t *Task) Stop() {
t.mux.Lock()
defer t.mux.Unlock()
t.running = 0
if t.ticker != nil {
t.ticker.Reset(t.every)
}
}
func (t *Task) Restart() {
t.running = 1
t.Run()
}
func (t *Task) Remove() {
t.Stop()
t = nil
}
func (t *Task) Running() bool {
return t.running == 1
}
func (t *Task) Stopped() bool {
return t.running == 0
}
`
I guess you should check your comment and correct the usage of the ``` code block.
What I can see, from what I can see, it seems that you use a mutex in dispatched code. I think that is not necessarily needed because all code that you dispatch will run sequential in the UI go routine: https://go-app.dev/concurrency#ui-goroutine
@oderwat thanks for the answer (``` code block done , my apologies), but if you put something like a read or a rest call and you change something outside of the task , something maybe like this in another piece of the code that you also change in the task. But understood the ui-goroutine
c.tsk.Lock()
err := ctx.LocalStorage().Get("authUser", &loginUser)
if err != nil {
app.Log("Error reading")
}
c.tsk.Unlock()
I did not check, but can you even reliably use ctx.LocalStorage() outside the UI go routine? I would not try to do that. You need to have ctx for that, which most likely is inside a handler or OnWhatever() and they all run inside the ui-goroutine anyway.
For our projects, we stay with all component modification code inside the ui-goroutine and the same for accessing local storage or basically anything DOM/JavaScript. All concurrent code ends up with some kind of dispatching that result.
Local storage is goroutine safe https://github.com/maxence-charriere/go-app/blob/master/pkg/app/storage.go#L106
Thanks @oderwat and @maxence-charriere , so we can safely remove the mutex. We are scheduling the tasks in the OnMount and they work beautifully. Thanks for your help