gui/docs/ref/wails-v3/migration/v2-to-v3.mdx
Snider 4bdbb68f46
Some checks failed
Security Scan / security (push) Failing after 9s
Test / test (push) Failing after 1m21s
refactor: update import path from go-config to core/config
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-14 10:26:36 +00:00

727 lines
17 KiB
Text

---
title: Migrating from v2 to v3
description: Complete guide to migrating your Wails v2 application to v3
sidebar:
order: 1
---
import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components";
Wails v3 is a **complete rewrite** with significant improvements in architecture, performance, and developer experience. This guide helps you migrate your v2 application to v3.
**Key changes:**
- New application structure
- Improved bindings system
- Enhanced window management
- Better event system
- Simplified configuration
**Migration time:** 1-4 hours for typical applications
## Breaking Changes
### Application Initialisation
In v2, application setup, window configuration, and execution were all combined into a single `wails.Run()` call. This monolithic approach made it difficult to create multiple windows, handle errors at different stages, or test individual components of your application.
v3 separates these concerns into distinct phases: application creation, window creation, and execution. This separation gives you explicit control over each stage of your application's lifecycle and makes the code more modular and testable.
**v2:**
```go
err := wails.Run(&options.App{
Title: "My App",
Width: 1024,
Height: 768,
Bind: []interface{}{
&GreetService{},
},
})
```
**v3:**
```go
app := application.New(application.Options{
Name: "My App",
Services: []application.Service{
application.NewService(&GreetService{}),
},
})
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My App",
Width: 1024,
Height: 768,
})
app.Run()
```
**Why this is better:**
- **Multi-window support**: You can create windows dynamically at any point, not just at startup
- **Better error handling**: Each phase can be validated separately with proper error handling
- **Clearer code**: The separation makes it obvious what's happening at each stage
- **More testable**: You can test application setup without running the event loop
- **More flexible**: Windows can be created, destroyed, and recreated throughout the application lifecycle
### Bindings
In v2, every bound struct required a context field and a `startup(ctx)` method to receive the runtime context. This created tight coupling between your business logic and the Wails runtime, making code harder to test and understand.
v3 introduces the service pattern, where your structs are completely standalone and don't need to store runtime context. If a service needs access to the application instance, it explicitly receives it through dependency injection rather than implicit context threading.
**v2:**
```go
type App struct {
ctx context.Context
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
func (a *App) Greet(name string) string {
return "Hello " + name
}
```
**v3:**
```go
type GreetService struct{}
func (g *GreetService) Greet(name string) string {
return "Hello " + name
}
// Register as service
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{}),
},
})
```
**Why this is better:**
- **No implicit dependencies**: Services are plain Go structs without hidden runtime dependencies
- **Easier testing**: You can test service methods without mocking a Wails context
- **Clearer code**: Dependencies are explicit (passed as constructor arguments) rather than hidden in a context field
- **Better organization**: Services can be grouped by domain rather than all living in a single `App` struct
- **Proper initialization**: Use `ServiceStartup()` method when you need initialization, making it explicit
### Runtime
In v2, all runtime operations required passing a context to global functions from the `runtime` package. This created tight coupling to the context object throughout your codebase and made the API feel procedural rather than object-oriented.
v3 replaces the context-based runtime with direct method calls on application and window objects. Operations are called directly on the objects they affect, making the code more intuitive and object-oriented.
**v2:**
```go
import "github.com/wailsapp/wails/v2/pkg/runtime"
runtime.WindowSetTitle(a.ctx, "New Title")
runtime.EventsEmit(a.ctx, "event-name", data)
```
**v3:**
```go
// Store app reference
type MyService struct {
app *application.Application
}
func (s *MyService) UpdateTitle() {
window := s.app.Window.Current()
window.SetTitle("New Title")
}
func (s *MyService) EmitEvent() {
s.app.Event.Emit("event-name", data)
}
```
**Why this is better:**
- **Object-oriented design**: Methods are called on the objects they affect (window, app, menu, etc.)
- **Clearer intent**: `window.SetTitle()` is more obvious than `runtime.WindowSetTitle(ctx, ...)`
- **Better IDE support**: Autocomplete works properly when methods are on objects
- **Multi-window clarity**: With multiple windows, you explicitly choose which window to operate on
- **No context threading**: You don't need to pass context through every function
### Frontend Bindings
In v2, bindings were organized by Go package and struct name, typically resulting in paths like `wailsjs/go/main/App`. This structure didn't reflect logical grouping and made it hard to find related functionality.
v3 organizes bindings by service name and application module, creating a clearer logical structure. The bindings are generated into a `bindings` directory organized by your application name and service names, making it easier to understand what functionality is available.
**v2:**
```javascript
import { Greet } from '../wailsjs/go/main/App'
const result = await Greet("World")
```
**v3:**
```javascript
import { Greet } from './bindings/myapp/greetservice'
const result = await Greet("World")
```
**Why this is better:**
- **Logical organization**: Bindings are grouped by service name rather than Go package structure
- **Clearer imports**: The path reflects the domain logic (greetservice) not the file structure (main/App)
- **Better discoverability**: You can navigate bindings by feature rather than by technical structure
- **Consistent naming**: Service-based organization matches your backend architecture
- **Simpler paths**: No more `../wailsjs/go` prefix - just `./bindings`
### Events
In v2, events used variadic `interface{}` parameters and required passing context to every event function. Event handlers received untyped data that needed manual type assertions, making the event system error-prone and hard to debug.
v3 introduces typed event objects and removes the context requirement. Event handlers receive a proper event object with typed data, making the event system more reliable and easier to use.
**v2:**
```go
runtime.EventsOn(ctx, "event-name", func(data ...interface{}) {
// Handle event
})
runtime.EventsEmit(ctx, "event-name", data)
```
**v3:**
```go
app.Event.On("event-name", func(e *application.CustomEvent) {
data := e.Data
// Handle event
})
app.Event.Emit("event-name", data)
```
**Why this is better:**
- **Type safety**: Events use proper event objects instead of `...interface{}`
- **Better debugging**: Event objects contain metadata like event name, making debugging easier
- **Clearer API**: `app.Event.On()` and `app.Event.Emit()` are more intuitive than runtime functions
- **No context needed**: Events work directly on the app object without threading context
- **Simpler handlers**: Event handlers have a clear signature instead of variadic parameters
### Windows
v2 supported only a single window per application. The window was created at startup and all window operations were performed through runtime functions that implicitly targeted that single window.
v3 introduces native multi-window support as a core feature. Each window is a first-class object with its own methods and lifecycle. You can create, manage, and destroy multiple windows dynamically throughout your application's lifetime.
**v2:**
```go
// Single window only
runtime.WindowSetSize(ctx, 800, 600)
```
**v3:**
```go
// Multiple windows supported
window1 := app.Window.New()
window1.SetSize(800, 600)
window2 := app.Window.New()
window2.SetSize(1024, 768)
```
**Why this is better:**
- **Multi-window applications**: Build apps with multiple independent windows (dashboards, preferences, tools, etc.)
- **Explicit window references**: Each window is an object you can store and manipulate directly
- **Dynamic window creation**: Create and destroy windows at any time during runtime
- **Independent window state**: Each window has its own events, properties, and lifecycle
- **Better architecture**: Window management is object-oriented rather than context-based
## Migration Steps
### Step 1: Update Dependencies
**go.mod:**
```go
module myapp
go 1.21
require (
github.com/wailsapp/wails/v3 v3.0.0-alpha.1
)
```
**Update:**
```bash
go get github.com/wailsapp/wails/v3@latest
go mod tidy
```
### Step 2: Update main.go
**v2:**
```go
package main
import (
"embed"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "My App",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
Bind: []interface{}{
app,
},
Windows: &windows.Options{
WebviewIsTransparent: false,
},
})
if err != nil {
println("Error:", err.Error())
}
}
```
**v3:**
```go
package main
import (
"embed"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed frontend/dist
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "My App",
Services: []application.Service{
application.NewService(&MyService{}),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
})
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My App",
Width: 1024,
Height: 768,
})
err := app.Run()
if err != nil {
panic(err)
}
}
```
### Step 3: Convert App Struct to Service
**v2:**
```go
type App struct {
ctx context.Context
}
func NewApp() *App {
return &App{}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
// Initialisation
}
func (a *App) Greet(name string) string {
return "Hello " + name
}
```
**v3:**
```go
type MyService struct {
app *application.Application
}
func NewMyService(app *application.Application) *MyService {
return &MyService{app: app}
}
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
// Initialisation
return nil
}
func (s *MyService) Greet(name string) string {
return "Hello " + name
}
// Register after app creation
app := application.New(application.Options{})
app.RegisterService(application.NewService(NewMyService(app)))
```
### Step 4: Update Runtime Calls
**v2:**
```go
func (a *App) DoSomething() {
runtime.WindowSetTitle(a.ctx, "New Title")
runtime.EventsEmit(a.ctx, "update", data)
runtime.LogInfo(a.ctx, "Message")
}
```
**v3:**
```go
func (s *MyService) DoSomething() {
window := s.app.Window.Current()
window.SetTitle("New Title")
s.app.Event.Emit("update", data)
s.app.Logger.Info("Message")
}
```
### Step 5: Update Frontend
**Generate new bindings:**
```bash
wails3 generate bindings
```
**Update imports:**
```javascript
// v2
import { Greet } from '../wailsjs/go/main/App'
// v3
import { Greet } from './bindings/myapp/myservice'
```
**Update event handling:**
```javascript
// v2
import { EventsOn, EventsEmit } from '../wailsjs/runtime/runtime'
EventsOn("update", (data) => {
console.log(data)
})
EventsEmit("action", data)
// v3
import { Events } from '@wailsio/runtime'
Events.On("update", (data) => {
console.log(data)
})
Events.Emit("action", data)
```
### Step 6: Update Configuration
**v2 (wails.json):**
```json
{
"name": "myapp",
"outputfilename": "myapp",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",
"frontend:dev:serverUrl": "auto"
}
```
**v3 (wails.json):**
```json
{
"name": "myapp",
"frontend": {
"dir": "./frontend",
"install": "npm install",
"build": "npm run build",
"dev": "npm run dev",
"devServerUrl": "http://localhost:5173"
}
}
```
## Feature Mapping
### dialogs
**v2:**
```go
selection, err := runtime.OpenFileDialog(ctx, runtime.OpenDialogOptions{
Title: "Select File",
})
```
**v3:**
```go
selection, err := app.Dialog.OpenFile(application.OpenFileDialogOptions{
Title: "Select File",
})
```
### Menus
**v2:**
```go
menu := menu.NewMenu()
menu.Append(menu.Text("File", nil, []*menu.MenuItem{
menu.Text("Quit", nil, func(_ *menu.CallbackData) {
runtime.Quit(ctx)
}),
}))
```
**v3:**
```go
menu := app.NewMenu()
fileMenu := menu.AddSubmenu("File")
fileMenu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
```
### System Tray
**v2:**
```go
// Not available in v2
```
**v3:**
```go
systray := app.SystemTray.New()
systray.SetIcon(iconBytes)
systray.SetLabel("My App")
menu := app.NewMenu()
menu.Add("Show").OnClick(showWindow)
menu.Add("Quit").OnClick(app.Quit)
systray.SetMenu(menu)
```
## Common Issues
### Issue: Bindings not found
**Problem:** Import errors after migration
**Solution:**
```bash
# Regenerate bindings
wails3 generate bindings
# Check output directory
ls frontend/bindings
```
### Issue: Context errors
**Problem:** `ctx` not available
**Solution:**
Store app reference instead:
```go
type MyService struct {
app *application.Application
}
func NewMyService(app *application.Application) *MyService {
return &MyService{app: app}
}
```
### Issue: Window methods not working
**Problem:** `runtime.WindowSetTitle()` doesn't exist
**Solution:**
Use window methods directly:
```go
window := s.app.Window.Current()
window.SetTitle("New Title")
```
### Issue: Events not firing
**Problem:** Events registered but not received
**Solution:**
Check event names match exactly:
```go
// Go
app.Event.Emit("my-event", data)
// JavaScript
OnEvent("my-event", handler) // Must match exactly
```
## Testing Migration
### Checklist
- [ ] Application starts without errors
- [ ] All bindings work
- [ ] Events are sent and received
- [ ] Windows open and close correctly
- [ ] Menus work (if applicable)
- [ ] dialogs work (if applicable)
- [ ] System tray works (if applicable)
- [ ] Build process works
- [ ] Production build works
### Test Commands
```bash
# Development
wails3 dev
# Build
wails3 build
# Generate bindings
wails3 generate bindings
```
## Benefits of v3
### Performance
- **Faster startup** - Optimised initialisation
- **Lower memory** - Efficient resource usage
- **Better bridge** - &lt;1ms call overhead
### Features
- **Multi-window** - Native support
- **System tray** - Built-in
- **Better events** - Typed, simpler API
- **Services** - Better code organisation
### Developer Experience
- **Type safety** - Full TypeScript support
- **Better errors** - Clear error messages
- **Hot reload** - Faster development
- **Better docs** - Comprehensive guides
## Getting Help
### Resources
- [Documentation](/quick-start/why-wails)
- [Discord Community](https://discord.gg/JDdSxwjhGf)
- [GitHub Issues](https://github.com/wailsapp/wails/issues)
- [Examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples)
### Common Questions
**Q: Can I run v2 and v3 side by side?**
A: Yes, they use different import paths.
**Q: Is v3 production-ready?**
A: v3 is in alpha/beta. Test thoroughly before production.
**Q: Will v2 be maintained?**
A: Yes, v2 will receive critical updates.
**Q: How long does migration take?**
A: 1-4 hours for typical applications.
## Next Steps
<CardGrid>
<Card title="Quick Start" icon="rocket">
Get started with Wails v3.
[Learn More →](/quick-start/installation)
</Card>
<Card title="Core Concepts" icon="star">
Understand v3 architecture.
[Learn More →](/concepts/architecture)
</Card>
<Card title="Bindings" icon="puzzle">
Learn the new bindings system.
[Learn More →](/features/bindings/methods)
</Card>
<Card title="Examples" icon="open-book">
See complete v3 examples.
[View Examples →](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or [open an issue](https://github.com/wailsapp/wails/issues).