MVP 16 adds scoped context with selector-based consumption. It is not a global store, router context, Redux clone, or deep comparison system.
type Context[T any]
func CreateContext[T any](defaultValue T) *Context[T]
func ProvideContext[T any](ctx *Context[T], value T)
func UseContext[T any](ctx *Context[T]) T
func UseContextSelector[T any, S comparable](
ctx *Context[T],
selector func(T) S,
) SProviders are ordinary components that call ProvideContext during render:
var PreferencesContext = gf.CreateContext(defaultPreferences())
func PreferencesProvider(props PreferencesProviderProps) gf.Node {
preferences, dispatch := gf.UseReducer(defaultPreferences(), reducePreferences)
gf.ProvideContext(PreferencesContext, preferences)
return (
<section>
<PreferencesControls Dispatch={dispatch} />
{props.Children}
</section>
)
}This avoids a new GOX provider syntax. Scope follows the mounted component tree through parent links. Descendant consumers read the nearest provider. Nested providers override outer providers for their own descendants.
ProvideContext must be called during component render. Calling it outside
render panics with a focused message.
Use selectors when a consumer only needs one comparable part of the context:
density := gf.UseContextSelector(PreferencesContext, func(value Preferences) string {
return value.Density
})The selector result type S must be comparable. The runtime compares the
previous selected value and the next selected value with ==. It does not use
reflection, unsafe, or deep equality.
Selectors should be deterministic and side-effect-free. They may close over stable values, but they should not mutate state or depend on changing external state.
If a selector panics during provider notification or provider topology refresh,
GoFrame reports gf.ErrorPhaseContext, keeps the previous selected value, and
does not mark the consumer dirty from that failed selector evaluation. If a
selector panics during the consumer's own render before a stable previous value
exists, the panic is reported as a context failure and then flows through normal
component render error handling. A nearest scoped gf.ErrorBoundary can catch
that initial render-path failure, but it does not catch later provider
notification failures.
During a provider topology refresh, structural subscription ownership advances to the new nearest provider even when its selector evaluation fails. The old or shadowed provider no longer notifies that subscription, while a later safe update from the new provider retries the selector and can recover the consumer. When the context default becomes nearest, the subscription detaches from its previous provider. The previous selected value remains committed and the consumer remains clean until a selector evaluation succeeds.
UseContext(ctx) returns the full nearest value. Because the runtime cannot
compare arbitrary T without reflection or a user-provided equality function,
UseContext subscribes broadly. Provider updates rerender broad consumers.
Use it for low-frequency or simple consumers. Prefer UseContextSelector for
components on hot update paths.
Each provider component stores its current context values. Each consumer stores its context subscriptions in positional context slots. When a provider updates:
- The provider value is updated during provider render.
- Selector subscribers under that provider recompute their selected value.
- Only subscribers whose selected value changed are marked dirty.
- Dirty descendant accounting prevents memoized ancestors from hiding those consumer updates.
Consumers under nested providers subscribe to the inner provider, not the outer provider. Unmounted consumers unsubscribe, and unmounted providers detach their subscribers.
Provider topology changes are observable. If a provider appears above an existing consumer, disappears, or a nearer nested provider appears between an outer provider and a consumer, affected subscriptions rebind to the new nearest provider. A successful selector evaluation marks the consumer dirty even when the comparable result is equal, because the provider scope itself changed. A failed selector evaluation leaves the previous selection committed and does not dirty the consumer, while keeping the new structural provider binding.
Context selectors are designed to work with explicit MemoEqual bailouts.
If a parent/provider rerenders, clean memoized descendants may skip. A context consumer whose selected value changed is marked dirty, so memoized ancestors above it cannot skip over the update.
Memoized ancestors also cannot hide successful context topology changes. When a provider appears, is removed, or a nearer nested provider takes over and its selector succeeds, the runtime marks affected consumers dirty and updates dirty descendant accounting through any memoized wrappers above them.
This means selector consumers should usually be component boundaries, and clean
structural wrappers can use explicit MemoEqual where useful.
examples/context demonstrates:
- scoped provider hook;
- density, accent, and counter selector consumers;
- a broad
UseContextconsumer; - nested provider override;
- memoized static leaves;
- browser smoke assertions that only the selected consumer rerenders.
Run it locally:
goxc package ./examples/context --compiler=tinygo
goxc serve ./examples/context --port=8080- Selector results must be comparable.
- There is no custom equality function for non-comparable selections yet.
- Context is synchronous and render-time only.
- No server context, async context, or context devtools.
- Context is not a global store; provider scope is the component tree.
- Context selectors do not replace reducer dispatch, explicit memoization, virtualization, or the hash router's route state.