This tutorial follows the reference app in examples/router-dashboard: a small
Go-first browser/WASM dashboard with a stable shell, hash routes, URL query
filters, a component-scoped resource owner, explicit loading and failed states,
controlled form validation, and a scoped render Error Boundary.
The goal is not to introduce a new app framework layer. The app uses the existing runtime primitives directly:
- GOX package-qualified components;
gf.NewHashRouter,gf.RouterView, andgf.RouterLink;gf.RouteContext.Query()andgf.WithQuery;gf.UseResourcefor one component-scoped data load;gf.UseReducerfor form state;gf.ErrorBoundaryfor render failures only.
Install Go, TinyGo, Node.js, Chrome or Chromium, gzip, brotli, and optionally
zstd. The repository CI currently uses Go 1.24.x, TinyGo 0.41.1, and
Node.js 20.
For a published install:
go install github.com/graybuton/goframe/cmd/goxc@latestInside this repository, install the local checkout:
go install ./cmd/goxc
goxc doctorPackage and serve the reference dashboard:
goxc package ./examples/router-dashboard --compiler=tinygo
goxc serve ./examples/router-dashboard --port=8080Open http://127.0.0.1:8080.
TinyGo is the normal path for the router, resource, query, and form scenarios. The intentional panic recovery demo in the Error Boundaries section requires a recover-capable Go/WASM build:
goxc package ./examples/router-dashboard --compiler=go
goxc serve ./examples/router-dashboard --port=8080Try these routes:
#/#/issues#/issues?status=open&q=auth#/issues/RD-2#/issues/RD-2/edit#/missing
The reference app uses the recommended Go-first child-entry layout:
examples/router-dashboard/
├── goframe.json
├── assets/
│ ├── index.html
│ ├── styles.css
│ └── data/
│ └── issues.txt
├── cmd/app/
│ ├── main.go
│ └── app.gox
└── internal/
├── data/
├── filters/
├── forms/
├── layout/
└── pages/
cmd/app is the executable entry package. internal/... packages keep UI,
data parsing/loading, forms, filters, and pages private to the app.
goxc generates .gox.go files under .goframe, not next to source files.
GOX lowercase tags create DOM elements:
<section class="rd-card">
<h2>Issues</h2>
</section>
Capitalized tags create component boundaries. Cross-package components use ordinary Go imports and package-qualified GOX tags:
import layout "github.com/graybuton/goframe/examples/router-dashboard/internal/layout"
func App() gf.Node {
return (
<layout.Shell>
{gf.RouterView(router)}
</layout.Shell>
)
}
The supported package-qualified form is packageAlias.Component. There are no
namespace tags, spread props, or file-based route components.
The app keeps layout outside route content:
App
-> data.IssueProvider
-> layout.Shell
-> gf.RouterView(router)
The shell stays mounted while routes, query filters, forms, and data reloads change the outlet. This is the recommended layout model for the current hash router.
Routes are declared in Go:
var router = gf.NewHashRouter([]gf.Route{
gf.RoutePath("/", homeRoute),
gf.RoutePath("/issues", issuesRoute),
gf.RoutePath("/issues/:id/edit", issueEditRoute),
gf.RoutePath("/issues/:id", issueDetailsRoute),
gf.NotFoundRoute(notFoundRoute),
})Route params come from gf.RouteContext:
id := ctx.Param("id")The router is hash-based. #/issues/RD-2 works on static hosting because the
server still serves index.html.
The issues page reads query state from the route:
query := ctx.Query()
filter := data.IssueFilter{
Query: query.Get("q"),
Status: query.Get("status"),
}Filter controls update the URL explicitly:
gf.Navigate(gf.WithQuery("/issues", gf.QueryValues{
"q": {query},
"status": {status},
}))There is no automatic query-state manager. The URL is just ordinary app state that components read and write deliberately.
The reference app has one stable resource owner. It loads the packaged
assets/data/issues.txt asset once and distributes the explicit resource
state through app-local context.
Important properties:
- initial app load starts one generation;
- route navigation does not reload data;
- query changes do not reload data;
- browser back/forward does not reload data;
- manual reload starts a new generation;
- failed state is rendered explicitly;
- there is no global cache or route loader.
The data loader is example-local browser code. GoFrame does not provide a runtime fetch API.
Resource state is normal UI state:
- loading shows a small loading panel while the shell remains mounted;
- ready pages render list, detail, and edit views from the loaded data;
- failed shows an error panel plus retry control.
Ordinary resource failure is not a render failure and does not activate an Error Boundary.
The edit page uses controlled inputs and a reducer:
Value={state.Title.Value};OnInput={func(event gf.InputEvent) { ... }};OnSubmit={func(event gf.Event) { event.PreventDefault(); ... }};- local touched/dirty/submitted flags;
- synchronous validation functions in the app package.
There is no schema validation framework, server mutation API, optimistic mutation layer, or persistence story in this tutorial.
Route content is wrapped in a scoped render Error Boundary. It protects route rendering from render panics and keeps the outer shell alive.
The reference app includes an intentional route-render failure for integration testing:
#/issues/RD-2?panic=render
That query parameter makes the detail route panic during render. The boundary
shows fallback UI, while the outer shell and component-scoped resource owner
stay mounted. The resource remains ready; this is not an ordinary
ResourceFailed state and it does not reload the data.
Run this demo with the Go/WASM package path:
goxc package ./examples/router-dashboard --compiler=go
goxc serve ./examples/router-dashboard --port=8080The size-oriented TinyGo build path does not guarantee recover-based containment and may trap instead of rendering the fallback. The rest of the router/resource/forms tutorial flow works through TinyGo.
The fallback exposes two different actions:
- retry the current route, which rerenders the same protected subtree with the same route/query input;
- navigate safely back to
/issues, which removes the crashing query input and returns the app to a normal route.
Reset does not fix the cause of an error automatically. If the same route props or query still trigger a panic, fallback UI can appear again.
Error Boundaries do not catch:
- ordinary
ResourceFailedstate; - event handler panics;
- effect panics;
- async resource failures;
- route loader failures, because route loaders do not exist yet.
For local development:
goxc package ./examples/router-dashboard --compiler=tinygo
goxc serve ./examples/router-dashboard --port=8080For cache-safe deploy-style artifacts:
goxc package ./examples/router-dashboard \
--compiler=tinygo \
--asset-hash \
--preload \
--compress=gzip,brUse goxc export only when you want a visible deploy directory.
Recommended learning path:
examples/counter: minimal state and packaging.examples/components: GOX components, props, children, and fragments.examples/router: focused routing.examples/resource: focused resource lifecycle, stale completion, failure, and cleanup behavior.examples/router-dashboard: integrated reference app.examples/dashboard: pressure/performance example.
Focused deep dives:
examples/todo: controlled inputs, effects, keys, and list helpers.examples/context: scoped providers and selector consumers.examples/virtualized: bounded DOM for fixed-height lists and tables.examples/multipackageandexamples/cmdapp: workspace and layout shapes.
GoFrame is experimental and not production-ready. The tutorial intentionally does not cover or provide:
- SSR or hydration;
- history-mode routing;
- route loaders;
- server resources or server functions;
- global resource cache;
- request deduplication;
- Suspense-style async rendering;
- schema validation;
- mutation framework;
- auth guards or route middleware;
- production deployment server;
- LSP or formatter;
- Player/Engine runtime.