MVP 9 adds a minimal lifecycle model for component-owned side effects. It is designed for Go-first applications, browser/WASM size constraints, and clear cleanup semantics. It is not React Fiber, Suspense, or a general lifecycle framework.
Effects are collected while a component renders and are flushed only after the runtime has patched the DOM. A state update from an effect schedules a later browser update instead of mutating the tree during render.
type Cleanup func()
func UseUnmount(cleanup Cleanup)
func UseEffect(effect func() Cleanup, deps ...EffectDeps)
type EffectDeps struct {
// internal lightweight representation
}UseEffect(fn) runs once after mount. Dependencies are explicit primitive
values:
func Deps(values ...any) EffectDeps
func Once() EffectDeps
func EveryRender() EffectDepsDeps accepts strings, booleans, signed and unsigned integer types, floats,
and nil. Unsupported dependency types panic during render with a focused
message. The runtime intentionally does not use reflection or deep equality.
Complex values should be reduced to strings, counters, IDs, versions, or other
primitive dependency values by the application.
Deprecated compatibility helpers (UseMount, NoDeps, AlwaysDeps,
DepsOf, and Dep*) may remain during the experimental cleanup period, but
new code should use the API above.
Call UseEffect without dependencies:
gf.UseEffect(func() gf.Cleanup {
println("mounted")
return func() {
println("unmounted")
}
})The returned cleanup runs when the component instance unmounts. A key or component-name change creates a new instance, so the mount effect runs again.
UseUnmount registers cleanup without a mount body:
gf.UseUnmount(func() {
println("released")
})The latest cleanup registered at that hook position runs on unmount.
UseEffect runs after mount and after explicit dependency changes:
value := text
gf.UseEffect(func() gf.Cleanup {
documentTitleSet("Todo: " + value)
return nil
}, gf.Deps(value))When dependencies change, the previous cleanup runs before the next effect body. The latest cleanup also runs on unmount.
gf.EveryRender() means run after every component render:
gf.UseEffect(func() gf.Cleanup {
println("rendered")
return nil
}, gf.EveryRender())Cleanup runs when a component instance is removed through normal reconciliation paths:
- a conditional component disappears;
- a keyed component is removed from a list;
- a component key changes;
- a component identity changes;
- a fragment subtree containing a component is removed;
- a mounted application is replaced.
Unmount cleanups run while the DOM range still exists, but applications should not depend on this detail. Treat cleanup as the place to release timers, external event listeners, subscriptions, and retained browser resources.
Replacing the active application through gf.Mount follows the same cleanup
contract for same-root and different-root replacement. Each previous cleanup
runs once while that application's mounted range still exists; the runtime then
removes the range before mounting the replacement. Effects queued by the
released application do not run afterward. The DOM visibility during cleanup
is an implementation detail, not an API for reading or transferring old DOM
state.
Mount validates a missing target and a different target nested inside the
current root before effect cleanup, UseUnmount cleanup, application release,
mounted-range removal, or pending-work reset. A validation panic therefore
leaves the current application's effects and cleanup ownership intact. This is
an ordering guarantee for target validation; it does not make panic recovery a
portable TinyGo behavior.
If an effect body panics, the runtime reports gf.ErrorPhaseEffect through the
installed runtime error handler and does not register a cleanup for that failed
effect run. If an effect cleanup panics, the runtime reports
gf.ErrorPhaseEffectCleanup, clears that cleanup slot, and continues later
cleanup work where possible. UseUnmount cleanup panics report
gf.ErrorPhaseUnmountCleanup and do not stop other cleanup slots from running.
Scoped gf.ErrorBoundary components do not catch effect, effect cleanup, or
unmount cleanup panics; boundaries are render-only.
Effects use component-scoped positional hook slots, just like UseState.
Calls must stay in a stable order between renders.
Calling lifecycle hooks outside component render panics with a focused message. Changing a lifecycle hook kind at the same effect slot also panics.
Production builds keep lifecycle diagnostics as no-op stubs.
goframe_debug browser builds warn when:
State.Setis called after component unmount;State.Setis called during component render;- an effect-triggered update loop exceeds the MVP guard threshold.
The loop guard is intentionally small. It prevents obvious effect-to-state runaway loops from continuing forever in debug builds, but it is not a priority scheduler.
The Todo example uses effects to persist tasks:
UseEffect(fn)loads compact localStorage state after first mount;UseEffectwrites localStorage only when the encoded todo list changes;- encoding lives in the example, not in
pkg/goframe, so the runtime does not importencoding/json.
gf.UseResource builds on the same after-patch timing and cleanup model as
effects. A resource loader starts only after the component's render has been
patched, and its cleanup runs when the resource key changes, reload starts a
new generation, or the component unmounts.
Resource loaders are expected to release browser timers, abort controllers,
subscriptions, or other retained handles from their cleanup. Late resolve or
reject calls after cleanup are ignored by the resource generation guard.
Resource loader setup panics are resource-specific. In recover-capable builds,
the resource wrapper reports gf.ErrorPhaseEffect, completes the current
generation as failed if panic was the first completion, and lets the internal
effect slot finish. A same-key rerender therefore does not retry a panicking
loader automatically. Use reload or a key change to start a new generation.
This does not change ordinary UseEffect body panic behavior.
- No
UseEffectdependency inference. - No cleanup ordering guarantees beyond each component instance cleaning its own registered hooks.
- No lifecycle hooks for before-render or before-patch.
- No automatic component memoization in the GOX compiler or runtime. Memoization is
explicit via
MemoEqualon component props and is intentionally opt-in. - No route-aware effect lifecycle, SSR, hydration, Suspense, or async component model.
- Hook order changes are unsupported and may panic only when the slot type changes.