@@ -74,6 +74,16 @@ Key Points:
7474- Uses Tailwind v4 for styling - you can use any Tailwind classes in your components.
7575- Use React-style camel case props (` className ` , ` onClick ` )
7676
77+ ## Quick Reference
78+
79+ - Component: app.DefineComponent("Name", func(props PropsType) any { ... })
80+ - Element: vdom.H("div", map[ string] any{"className": "..."}, children...)
81+ - Local state: atom := app.UseLocal(initialValue); atom.Get(); atom.Set(value)
82+ - Event handler: "onClick": func() { ... }
83+ - Conditional: vdom.If(condition, element)
84+ - Lists: vdom.ForEach(items, func(item, idx) any { return ... })
85+ - Styling: "className": vdom.Classes("bg-gray-900 text-white p-4", vdom.If(cond, "bg-blue-800")) // Tailwind + dark mode
86+
7787## Building Elements with vdom.H
7888
7989The vdom.H function creates virtual DOM elements following a React-like pattern (React.createElement). It takes a tag name, a props map, and any number of children:
@@ -220,7 +230,6 @@ Helper functions:
220230- ` vdom.Ternary[T any](cond bool, trueRtn T, falseRtn T) T ` - Type-safe ternary operation, returns trueRtn if condition is true, falseRtn otherwise
221231- ` vdom.ForEach[T any](items []T, fn func(T, int) any) []any ` - Maps over items with index, function receives item and index
222232- ` vdom.Classes(classes ...any) string ` - Combines multiple class values into a single space-separated string, similar to JavaScript clsx library (accepts string, [ ] string, and map[ string] bool params)
223- - ` app.DeepCopy[T any](value T) T ` - Creates a deep copy of slices, maps, and other complex types for safe state updates
224233
225234- The vdom.If and vdom.IfElse functions can be used for both conditional rendering of elements, conditional classes, and conditional props.
226235- For vdom.If and vdom.IfElse, always follow the pattern of condition first (bool), then value(s).
@@ -353,6 +362,65 @@ var MyComponent = app.DefineComponent("MyComponent", func(_ struct{}) any {
353362})
354363```
355364
365+ ** Never mutate values from Get()** : For complex data types, never modify the value returned from ` atom.Get() ` . Always use ` app.DeepCopy() ` before mutations:
366+
367+ ``` go
368+ var MyComponent = app.DefineComponent (" MyComponent" , func (_ struct {}) any {
369+ todos := app.UseLocal ([]Todo {{Text: " Learn Tsunami" }})
370+
371+ addTodo := func () {
372+ // ✅ Correct: Copy before modifying
373+ todos.SetFn (func (current []Todo) []Todo {
374+ todosCopy := app.DeepCopy (current)
375+ return append (todosCopy, Todo{Text: " New task" })
376+ })
377+ }
378+
379+ // ❌ Wrong: Never mutate the original
380+ // badUpdate := func() {
381+ // current := todos.Get()
382+ // current[0].Text = "Modified" // Dangerous mutation!
383+ // todos.Set(current)
384+ // }
385+
386+ return vdom.H (" div" , nil , " Todo count: " , len (todos.Get ()))
387+ })
388+ ```
389+
390+ ** Capture atoms, not values** : In closures and async code, always capture the atom itself, never captured values from render:
391+
392+ ``` go
393+ var MyComponent = app.DefineComponent (" MyComponent" , func (_ struct {}) any {
394+ count := app.UseLocal (0 )
395+ currentCount := count.Get () // Read in render
396+
397+ // ✅ Correct: Capture the atom
398+ handleDelayedIncrement := func () {
399+ time.AfterFunc (time.Second , func () {
400+ count.SetFn (func (current int ) int { return current + 1 })
401+ })
402+ }
403+
404+ // ❌ Wrong: Capturing stale value from render
405+ // handleStaleIncrement := func() {
406+ // time.AfterFunc(time.Second, func() {
407+ // count.Set(currentCount + 1) // Uses stale currentCount!
408+ // })
409+ // }
410+
411+ return vdom.H (" button" , map [string ]any{
412+ " onClick" : handleDelayedIncrement,
413+ }, " Count: " , currentCount)
414+ })
415+ ```
416+
417+ ** Key Points:**
418+
419+ - Use ` app.DeepCopy(value) ` before modifying complex data from ` atom.Get() `
420+ - Always capture atoms in closures, never captured render values
421+ - This prevents stale closures and shared reference bugs
422+ - ` app.DeepCopy[T any](value T) T ` works with slices, maps, structs, and nested combinations
423+
356424### Local State with app.UseLocal
357425
358426For component-specific state, use app.UseLocal:
@@ -677,38 +745,26 @@ app.UseRef creates mutable values that persist across renders without triggering
677745
678746``` go
679747var MyComponent = app.DefineComponent (" MyComponent" , func (_ struct {}) any {
680- // Store complex state that goroutines need to access
681- timerRef := app.UseRef (&TimerState{
682- active: false ,
683- done: make (chan bool ),
684- })
685-
686748 // Count renders without triggering re-renders
687749 renderCount := app.UseRef (0 )
688750 renderCount.Current ++
689751
690- startTimer := func () {
691- if timerRef.Current .active {
692- return
693- }
752+ // Store previous values for comparison
753+ prevCount := app.UseRef (0 )
754+ count := app.UseLocal (0 )
694755
695- timerRef.Current .active = true
696- go func () {
697- // Goroutine can safely access ref
698- for timerRef.Current .active {
699- time.Sleep (time.Second )
700- // Update UI state from goroutine
701- // count.Set(someValue)
702- app.SendAsyncInitiation ()
703- }
704- }()
756+ currentCount := count.Get ()
757+ if prevCount.Current != currentCount {
758+ fmt.Printf (" Count changed from %d to %d \n " , prevCount.Current , currentCount)
759+ prevCount.Current = currentCount
705760 }
706761
707762 return vdom.H (" div" , nil ,
708763 vdom.H (" p" , nil , " Render #" , renderCount.Current ),
764+ vdom.H (" p" , nil , " Count: " , currentCount),
709765 vdom.H (" button" , map [string ]any{
710- " onClick" : startTimer ,
711- }, " Start Timer " ),
766+ " onClick" : func () { count. Set (currentCount + 1 ) } ,
767+ }, " Increment " ),
712768 )
713769})
714770```
@@ -943,6 +999,19 @@ Key points:
943999- Content-Type is automatically detected for static files
9441000- For dynamic handlers, set Content-Type explicitly when needed
9451001
1002+ ## CRITICAL RULES (Must Follow)
1003+
1004+ ### Hooks (Same as React)
1005+
1006+ - ✅ Only call hooks at component top level, before any returns
1007+ - ❌ Never call hooks in loops, conditions, or after early returns
1008+
1009+ ### Atoms (Tsunami-specific)
1010+
1011+ - ✅ Read with atom.Get() in render code
1012+ - ❌ Never call atom.Set() in render code - only in handlers/effects
1013+ - ✅ Always use SetFn() for concurrent updates from goroutines
1014+
9461015## Tsunami App Template
9471016
9481017``` go
@@ -1020,18 +1089,18 @@ var App = app.DefineComponent("App", func(_ struct{}) any {
10201089 inputText.Set (" " )
10211090 }
10221091
1023- toggleTodo := func (id int ) {
1024- currentTodos := todos.Get ()
1025- newTodos := make ([]Todo, len (currentTodos) )
1026- copy (newTodos, currentTodos)
1027- for i := range newTodos {
1028- if newTodos [i].Id == id {
1029- newTodos[i]. Completed = !newTodos[i]. Completed
1030- break
1031- }
1032- }
1033- todos. Set (newTodos )
1034- }
1092+ toggleTodo := func (id int ) {
1093+ todos.SetFn ( func (current []Todo) []Todo {
1094+ todosCopy := app. DeepCopy (current )
1095+ for i := range todosCopy {
1096+ if todosCopy[i]. Id == id {
1097+ todosCopy [i].Completed = !todosCopy[i]. Completed
1098+ break
1099+ }
1100+ }
1101+ return todosCopy
1102+ } )
1103+ }
10351104
10361105 deleteTodo := func (id int ) {
10371106 currentTodos := todos.Get ()
@@ -1093,15 +1162,29 @@ Key points:
109311623 . Do NOT write a main() function - the framework handles app lifecycle
109411634 . Use init() for setup like registering dynamic handlers with app.HandleDynFunc
10951164
1165+ ## Common Mistakes to Avoid
1166+
1167+ 1 . ** Calling Set in render** : ` countAtom.Set(42) ` in component body causes infinite loops
1168+ 2 . ** Missing keys in lists** : Always use ` .WithKey(id) ` for list items
1169+ 3 . ** Stale closures in goroutines** : Use ` atom.Get() ` inside event handlers, effects, and goroutines, not captured values
1170+ 4 . ** Wrong prop format** : Use ` "className" ` not ` "class" ` , ` "onClick" ` not ` "onclick" ` (matching React prop and style names)
1171+ 5 . ** Mutating state** : Always create new slices/objects when updating atoms (can use app.DeepCopy helper)
1172+
1173+ ## Styling Requirements
1174+
1175+ ** IMPORTANT** : Tsunami apps run in Wave Terminal (dark mode). Always use dark-friendly styles:
1176+
1177+ - ✅ ` "bg-gray-900 text-white" `
1178+ - ✅ ` "bg-slate-800 border-gray-600" `
1179+ - ❌ ` "bg-white text-black" ` (avoid light backgrounds)
1180+
10961181## Important Technical Details
10971182
10981183- Props must be defined as Go structs with json tags
1099- - Components take their props type directly.
1184+ - Components take their props type directly as a parameter
11001185- Always use app.DefineComponent for component registration
1101- - Call app.SendAsyncInitiation after async state updates
11021186- Provide keys when using vdom.ForEach with lists (using WithKey method)
11031187- Use vdom.Classes with vdom.If for combining static and conditional class names
1104- - Consider cleanup functions in app.UseEffect for async operations
11051188- ` <script> ` tags are NOT supported
11061189- Applications consist of a single file: app.go containing all Go code and component definitions
11071190- Styling is handled through Tailwind v4 CSS classes
@@ -1110,4 +1193,9 @@ Key points:
11101193- This is a pure Go system - do not attempt to write React components or JavaScript code
11111194- All UI rendering, including complex visualizations, should be done through Go using vdom.H
11121195
1113- The todo demo demonstrates all these patterns in a complete application.
1196+ ** Async Operation Guidelines**
1197+
1198+ - Use app.UseGoRoutine instead of raw go statements for component-related async work
1199+ - Always respect ctx.Done() in app.UseGoRoutine functions to prevent goroutine leaks
1200+ - Use app.UseEffect with cleanup functions for subscriptions, timers, and other lifecycle management
1201+ - Call app.SendAsyncInitiation after state updates to trigger re-rendering
0 commit comments