Documentation Index
Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
The sync package includes a reactive state system inspired by SolidJS and Svelte. It provides Signals for state, Computed for derived values, and Effects for side effects. This enables automatic updates when data changes.
Core Concepts
Why Reactive State?
Without reactivity:
// Manual updates everywhere
count := 0
count++
updateUI(count) // Don't forget this!
updateOtherThing() // And this!
With reactivity:
// Automatic propagation
count := sync.NewSignal(0)
count.Set(1) // All dependents update automatically
A Signal is a reactive value container. When the value changes, all dependents are notified.
Creating Signals
import "github.com/go-mizu/mizu/view/sync"
// Signal with initial value
count := sync.NewSignal(0)
name := sync.NewSignal("Alice")
items := sync.NewSignal([]string{"a", "b", "c"})
Reading Values
value := count.Get() // Returns 0
When Get() is called inside a Computed or Effect, it automatically registers as a dependency.
Setting Values
count.Set(5) // All dependents are notified
Updating Values
// Update based on current value
count.Update(func(current int) int {
return current + 1
})
Full Example
count := sync.NewSignal(0)
// Read
fmt.Println(count.Get()) // 0
// Set
count.Set(10)
fmt.Println(count.Get()) // 10
// Update
count.Update(func(n int) int { return n * 2 })
fmt.Println(count.Get()) // 20
Computed
A Computed is a derived value that automatically recomputes when its dependencies change.
Creating Computed Values
count := sync.NewSignal(5)
// This automatically tracks count as a dependency
doubled := sync.NewComputed(func() int {
return count.Get() * 2
})
Reading Computed Values
fmt.Println(doubled.Get()) // 10
count.Set(7)
fmt.Println(doubled.Get()) // 14 (automatically recomputed)
Lazy Evaluation
Computed values are:
- Lazy - Only computed when accessed
- Cached - Not recomputed if dependencies haven’t changed
computeCount := 0
result := sync.NewComputed(func() int {
computeCount++
return count.Get() * 2
})
// Not computed yet (lazy)
fmt.Println(computeCount) // 0
result.Get()
fmt.Println(computeCount) // 1
result.Get() // Uses cache
fmt.Println(computeCount) // 1 (not recomputed)
count.Set(10) // Marks as dirty
result.Get() // Now recomputes
fmt.Println(computeCount) // 2
Chained Computed
Computed values can depend on other computed values:
count := sync.NewSignal(5)
doubled := sync.NewComputed(func() int {
return count.Get() * 2
})
quadrupled := sync.NewComputed(func() int {
return doubled.Get() * 2
})
fmt.Println(quadrupled.Get()) // 20
count.Set(3)
fmt.Println(quadrupled.Get()) // 12
An Effect runs a function whenever its dependencies change. Use it for side effects like logging, API calls, or UI updates.
Creating Effects
count := sync.NewSignal(0)
effect := sync.NewEffect(func() {
fmt.Printf("Count is now: %d\n", count.Get())
})
// Prints immediately: "Count is now: 0"
count.Set(5)
// Prints: "Count is now: 5"
count.Set(10)
// Prints: "Count is now: 10"
Stopping Effects
effect := sync.NewEffect(func() {
// ...
})
// Later, stop the effect
effect.Stop()
// Changes no longer trigger the effect
count.Set(100) // Nothing printed
Effect Use Cases
Logging:
sync.NewEffect(func() {
log.Printf("User changed: %+v", user.Get())
})
UI Updates:
sync.NewEffect(func() {
updateTodoList(todos.All())
})
Persistence:
sync.NewEffect(func() {
settings := settingsSignal.Get()
saveToStorage(settings)
})
Practical Examples
Counter
count := sync.NewSignal(0)
increment := func() {
count.Update(func(n int) int { return n + 1 })
}
decrement := func() {
count.Update(func(n int) int { return n - 1 })
}
// Display effect
sync.NewEffect(func() {
fmt.Printf("Counter: %d\n", count.Get())
})
increment() // Counter: 1
increment() // Counter: 2
decrement() // Counter: 1
email := sync.NewSignal("")
password := sync.NewSignal("")
emailValid := sync.NewComputed(func() bool {
e := email.Get()
return strings.Contains(e, "@") && len(e) > 3
})
passwordValid := sync.NewComputed(func() bool {
return len(password.Get()) >= 8
})
formValid := sync.NewComputed(func() bool {
return emailValid.Get() && passwordValid.Get()
})
// UI effect
sync.NewEffect(func() {
if formValid.Get() {
enableSubmitButton()
} else {
disableSubmitButton()
}
})
Filtered List
items := sync.NewSignal([]Item{...})
filter := sync.NewSignal("")
filteredItems := sync.NewComputed(func() []Item {
f := strings.ToLower(filter.Get())
if f == "" {
return items.Get()
}
var result []Item
for _, item := range items.Get() {
if strings.Contains(strings.ToLower(item.Name), f) {
result = append(result, item)
}
}
return result
})
// Update filter
filter.Set("search term")
// filteredItems.Get() now returns only matching items
Stats Dashboard
sales := sync.NewSignal([]Sale{...})
totalRevenue := sync.NewComputed(func() float64 {
var total float64
for _, s := range sales.Get() {
total += s.Amount
}
return total
})
averageSale := sync.NewComputed(func() float64 {
all := sales.Get()
if len(all) == 0 {
return 0
}
return totalRevenue.Get() / float64(len(all))
})
saleCount := sync.NewComputed(func() int {
return len(sales.Get())
})
Integration with Collections
Collections are reactive:
client := sync.New(opts)
todos := sync.NewCollection[Todo](client, "todo")
// This is reactive!
sync.NewEffect(func() {
all := todos.All() // Re-runs when todos change
fmt.Printf("Todo count: %d\n", len(all))
})
// Also reactive
todoCount := sync.NewComputed(func() int {
return todos.Count()
})
Thread Safety
All reactive primitives are thread-safe:
count := sync.NewSignal(0)
// Safe to call from multiple goroutines
go func() {
count.Set(1)
}()
go func() {
count.Set(2)
}()
go func() {
fmt.Println(count.Get())
}()
Best Practices
1. Keep Computations Pure
// Good: pure computation
doubled := sync.NewComputed(func() int {
return count.Get() * 2
})
// Bad: side effects in computed
bad := sync.NewComputed(func() int {
log.Println("Computing...") // Side effect!
return count.Get() * 2
})
Use Effects for side effects.
2. Avoid Deep Nesting
// Harder to follow
result := sync.NewComputed(func() int {
return sync.NewComputed(func() int { // Don't nest like this
return count.Get() * 2
}).Get()
})
// Better: flat structure
doubled := sync.NewComputed(func() int {
return count.Get() * 2
})
result := sync.NewComputed(func() int {
return doubled.Get()
})
3. Stop Effects When Done
effect := sync.NewEffect(func() { ... })
defer effect.Stop() // Clean up
4. Use Computed for Derived State
// Bad: manual syncing
items := sync.NewSignal([]Item{...})
count := sync.NewSignal(0)
sync.NewEffect(func() {
count.Set(len(items.Get())) // Manually keeping in sync
})
// Good: computed
items := sync.NewSignal([]Item{...})
count := sync.NewComputed(func() int {
return len(items.Get()) // Automatically derived
})
5. Break Down Complex Computations
// Hard to read
result := sync.NewComputed(func() Report {
items := items.Get()
filtered := filterItems(items, filter.Get())
sorted := sortItems(filtered, sortBy.Get())
paginated := paginateItems(sorted, page.Get(), pageSize.Get())
return buildReport(paginated)
})
// Better: intermediate computations
filteredItems := sync.NewComputed(func() []Item {
return filterItems(items.Get(), filter.Get())
})
sortedItems := sync.NewComputed(func() []Item {
return sortItems(filteredItems.Get(), sortBy.Get())
})
paginatedItems := sync.NewComputed(func() []Item {
return paginateItems(sortedItems.Get(), page.Get(), pageSize.Get())
})
report := sync.NewComputed(func() Report {
return buildReport(paginatedItems.Get())
})