Hot reloading implementation for GoFlow applications, enabling rapid development with automatic code reloading and state preservation.
✅ File Watcher: Automatically detects changes to .go files
✅ State Preservation: Preserves application state across reloads
✅ Widget Tree Diffing: Identifies changes in the widget tree
✅ Partial Rebuilds: Rebuilds only the changed parts of the widget tree
The hot reload system consists of four main components:
The main orchestrator that:
- Watches files for changes using
fsnotify - Triggers reloads when changes are detected
- Manages the reload lifecycle
- Provides debouncing to avoid excessive reloads
Thread-safe state storage that:
- Preserves application state across reloads
- Supports snapshotting and restoration
- Uses
sync.RWMutexfor concurrent access
Widget tree diffing engine that:
- Compares old and new widget trees
- Identifies added, removed, modified, and reordered widgets
- Recursively diffs structs, slices, maps, and primitives
Rebuild strategy manager that:
- Determines rebuild strategy (full, partial, or none)
- Identifies affected widget paths
- Manages dirty flags for widgets
import "github.com/base-go/GoFlow/pkg/hotreload"
// Create hot reloader
reloader, err := hotreload.NewHotReloader(hotreload.Config{
WatchPaths: []string{"./lib"},
ReloadFunc: func(ctx context.Context) error {
app.Rebuild()
return nil
},
EnableStatePreservation: true,
})
if err != nil {
log.Fatal(err)
}
// Start watching
if err := reloader.Start(); err != nil {
log.Fatal(err)
}
defer reloader.Stop()// Get state store
store := reloader.GetStateStore()
// Save state before reload
store.Set("counter", myCounter)
store.Set("username", myUsername)
// Restore state after reload
if val, ok := store.Get("counter"); ok {
myCounter = val.(int)
}
if val, ok := store.Get("username"); ok {
myUsername = val.(string)
}// Create differ
differ := hotreload.NewDiffer()
// Compare widget trees
diffs := differ.Diff(oldTree, newTree, []string{"root"})
// Process diffs
for _, diff := range diffs {
log.Printf("Change: %s at %v", diff.Type, diff.Path)
}// Create rebuild manager
manager := hotreload.NewPartialRebuildManager(store)
// Compute diffs
diffs := manager.ComputeDiff(oldWidget, newWidget)
// Determine strategy
strategy := manager.DetermineStrategy(diffs)
switch strategy {
case hotreload.RebuildNone:
// No rebuild needed
case hotreload.RebuildPartial:
// Rebuild only affected paths
paths := manager.GetAffectedPaths(diffs)
for _, path := range paths {
rebuildWidget(path)
}
case hotreload.RebuildFull:
// Full rebuild
app.Rebuild()
}See examples/hotreload-demo/main.go for a basic example showing:
- File watching
- State preservation
- Automatic reload on file changes
Run with:
cd examples/hotreload-demo
go run main.goSee examples/hotreload-widget-demo/main.go for an advanced example showing:
- Widget tree diffing
- Partial rebuild strategies
- Integration with GoFlow framework
Run with:
cd examples/hotreload-widget-demo
go run main.gotype Config struct {
// WatchPaths are the directories or files to watch
WatchPaths []string
// ReloadFunc is called when changes are detected
ReloadFunc ReloadFunc
// EnableStatePreservation enables state preservation across reloads
EnableStatePreservation bool
// Verbose enables verbose logging
Verbose bool
}The hot reloader automatically ignores:
- Non-
.gofiles _test.gofiles- Hidden files (starting with
.) - Temporary files
- File Watching: The
fsnotifylibrary watches for file system events - Event Filtering: Only relevant events (Write, Create, Remove, Rename) on
.gofiles are processed - Debouncing: Multiple rapid changes are debounced to avoid excessive reloads
- State Preservation: Current state is saved to the StateStore
- Reload Trigger: The reload function is called
- Tree Diffing: Old and new widget trees are compared
- Rebuild Strategy: Determines whether to do a full or partial rebuild
- State Restoration: Preserved state is restored
- Render: Updated UI is rendered
- DiffTypeNone: No change
- DiffTypeAdded: Widget or value added
- DiffTypeRemoved: Widget or value removed
- DiffTypeModified: Widget or value modified
- DiffTypeReordered: Children reordered or count changed
- RebuildNone: No rebuild needed (no changes detected)
- RebuildPartial: Rebuild only affected subtrees (optimized)
- RebuildFull: Rebuild entire tree (fallback for major changes)
- Use State Preservation: Save critical state to the StateStore
- Minimize State: Only preserve state that needs to survive reloads
- Test Reload Logic: Ensure your app handles reloads gracefully
- Watch Specific Paths: Only watch directories that contain your app code
- Handle Errors: Implement proper error handling in your reload function
- File watching has minimal overhead using native OS APIs
- State preservation uses efficient
sync.RWMutexfor concurrent access - Widget tree diffing uses reflection but is optimized for shallow comparisons
- Debouncing prevents excessive reloads during rapid file changes
- Currently only watches
.gofiles - Requires recompilation (not true "hot code swapping")
- Large widget trees may take longer to diff
- State preservation requires serializable types
- Support for watching asset files (images, fonts, etc.)
- Incremental compilation
- Better error recovery
- Hot reload metrics and profiling
- Custom file filters
- Integration with build tools
Contributions are welcome! Please see the main GoFlow contributing guidelines.
Part of the GoFlow framework. See main LICENSE file.