Fragment Callbacks and Lifecycle
Fragment callbacks handle resource ownership, measurement, coordinated multi-node changes, and other behavior that bindings, computed values, and constraints cannot express.
Start with the callback declarations and registration example in
View Fragments. This page
covers the lifecycle details that matter once callbacks own mutable state.
The central rule is that onUnmount releases a SceneGraph tree for reuse; it
does not imply that the fragment or its children were destroyed.
Status Restoration
The complete ViewStatus set is:
| Enum | AppConfig key | Intended use |
|---|---|---|
ViewStatus.None | None | Internal sentinel; not normally an authored visual bucket. |
ViewStatus.Normal | normal | Resting, enabled appearance. |
ViewStatus.Focused | focused | Focused but not selected. |
ViewStatus.FocusFootprint | focusFootprint | Last-focused footprint after active focus moves elsewhere. |
ViewStatus.Selected | selected | Selected but not focused. |
ViewStatus.Disabled | disabled | Disabled appearance. |
ViewStatus.Error | error | Validation or error appearance. |
ViewStatus.FocusSelected | focusSelected | Focused and selected together. |
A newly created fragment starts with ViewStatus.Normal. A host whose target
status is also Normal can therefore skip a views.normal pass. Put the
complete first normal appearance in views.base, then use views.normal to
restore a pooled fragment that returns from another status.
Status maps are sparse field updates. The provider does not replay
views.base, merge the new status over normal, or roll back the old status
first. If focused changes scale and selected omits it, a direct
Focused-to-Selected transition can retain the focused scale.
For every field changed by any status, set the correct value in every status
the application can reach directly. At minimum, make normal restore all
changed fields.
Fragment Tree Identity
ViewFragmentProvider recursively indexes descendants in
fragment.viewsById; nested children are supported.
- Give every bound, constrained, status-targeted, or callback-targeted child a stable ID.
- Keep IDs unique across the entire descendant tree. They are not scoped to a nested group.
- Duplicate IDs log a warning, and the later descendant replaces the earlier
entry in
viewsById. - Child IDs can repeat across different fragment instances because each instance has its own map.
- The root is not in
viewsById. Usefragment.viewfor the root andcellfor host geometry in constraints. - Do not give a child the same ID as the root; legacy
findNodefallback can make that tree ambiguous.
Avoid a fixed AppConfig root id on a reusable or prefetched fragment style.
It is copied to every pooled instance and can collide in the provider's global
fragment index and in SceneGraph lookup. When omitted, the provider creates a
unique fragment_# ID.
Registration Lifetime
Register every callback name before a screen, CollectionView prewarm, or provider preload can acquire the style:
import { AppUtils } from '@hs-src/hosanna-bridge-core/AppUtils';
import {
type ViewFragmentCallbackHandler,
ViewFragmentCallbackType,
} from '@hs-src/hosanna-ui/views/lib/AppConfig';
import type { IViewFragmentProvider } from '@hs-src/hosanna-ui/views/lib/ViewFragmentProvider';
const provider =
AppUtils.resolve<IViewFragmentProvider>('viewFragmentProvider');
provider.registerViewFragmentCallback<ViewFragmentCallbackHandler>(
ViewFragmentCallbackType.OnDataChange,
'configureMediaCard',
(host, fragment, data, status) => {
fragment.viewsById.progress.visible =
Number(data.progress ?? 0) > 0;
},
);
Names are resolved to function references when a fragment instance is created. Registering a name later does not retrofit instances that are checked out or waiting in the pool. Registering the same type and name again changes future instances; already-hydrated fragments keep the old reference.
If a regular callback name is missing at creation, the provider logs an error
and stores a no-op in that array position. If a ${fn.*} handler is missing,
the bound field is not updated, so a reused fragment can retain its previous
value.
onSpecificViewDataChange is the provider registration type used by
${fn.name(...)} field bindings. It is not a host lifecycle event and should
not be placed in the regular callback object.
The four lifecycle arrays also accept direct
AsyncFunctionPointer<ViewFragmentCallbackHandler> entries in programmatic
AppConfig, and one array can mix direct pointers with registered names. A
direct handler must be an exported module function with no captured this or
closure state:
export function resetCard(
host,
fragment,
data,
status,
): void {
fragment.viewsById.progress.visible = false;
}
appConfig.setConfig({
'viewFragments.mediaCard': {
views: { base: [], normal: {} },
callbacks: {
onUnmount: ['cancelMedia', resetCard],
},
},
});
JSON AppConfig cannot contain a function reference, so use registered string names there. See Async Function Pointers for the export and serialization rules.
Handler Contracts
onMount, onUnmount, onApplyViewStatus, and onDataChange use:
type ViewFragmentCallbackHandler = (
host: ICollectionViewCell | IHosannaView<ViewState>,
fragment: IViewFragment,
data: JsonData,
viewStatus: ViewStatus,
) => void;
| Argument | Contract |
|---|---|
host | Owning DynamicCell or FragmentView. Narrow it only for a deliberately host-specific style. |
fragment | Acquired instance. Use fragment.view for its root and fragment.viewsById for descendants. |
data | Current host content when available. Do not assume onUnmount receives the previously rendered item. |
viewStatus | Current host status when invoked. Some recycle paths have already reset the host to Normal. |
Track external work by fragment.id, not only by data.id. A DynamicCell
style change installs the new item before releasing the old fragment, while
cell recycling resets cell data before its unmount callback.
Property functions use a separate contract:
type ISpecificViewFragmentCallbackHandler = (
data: JsonData,
childView: ISGNNode,
field: string,
args: unknown[],
) => unknown;
The return value is assigned to childView[field]. Use a property function
for one field. Use onDataChange when behavior writes several children,
measures nodes, or owns resources.
Handlers in one array run synchronously in authored order. The provider does
not wrap each handler in try/catch; if one throws, later handlers in that
group do not run and the host operation can abort. Keep teardown handlers
defensive.
Host Ordering
DynamicCell
When a cell needs a different style or row container, the normal sequence is:
- invoke
onUnmounton the old fragment - release it
- acquire and append the new fragment
- invoke
onMount - apply direct and function data bindings
- evaluate computed values for the target status
- apply a changed status overlay
- apply constraints and invoke
onApplyViewStatus - invoke
onDataChange
The exact middle order varies when status is unchanged, but onDataChange
runs after binding and layout work for the content update. A newly acquired
DynamicCell invokes onMount before the new item's bound child fields are
ready. Read the supplied data or defer content-specific work to
onDataChange.
A status-only update evaluates focus-dependent computed values, applies the
status overlay and constraints, then invokes onApplyViewStatus. It skips the
callback when the requested status already matches the fragment.
FragmentView
For a mounted FragmentView:
- host mounting attaches the renderer and invokes
onMount updateData(...)applies bindings, performs its computed/constraint pass when constraints exist, then invokesonDataChange- a changed status evaluates computed values, applies the overlay, invokes
onApplyViewStatus, and can reapply storeditemContent recycle()and host unmount invokeonUnmountbefore returning the fragment to the pool
Initial state can prepare status and data before host mounting. Portable
callbacks must not depend on DynamicCell and FragmentView having identical
onMount data timing. Choose fragmentStyleKey before mount; the current
mounted setter does not replace the attached renderer, so live style switching
is not supported.
Header Fragments
A row header created through headerSettings.headerComponent can apply
direct/function bindings, computed values, and constraints when its style and
row data support them.
Header fragments do not run the full callback lifecycle and do not apply
status overlays. Declaring onMount, onDataChange, onApplyViewStatus, or
onUnmount on a header style does not make the header path invoke them.
Inheritance
AppConfig deep-merges the callbacks object by callback group:
- a group present only in the base style is inherited
- a new group in the derived style is added
- redefining a group replaces the entire base array; arrays are not concatenated
Repeat inherited names explicitly when a derived style adds a callback to an existing group.
Generated binding maps have a separate inheritance limitation. Do not rely on
a $supportsDataMap leaf inheriting compiled ${data.*}, ${fn.*}, or exact
computed.name view mappings from its base. Redeclare bound views.base
fields in each concrete leaf until that runtime defect is fixed. See
Fragment Data Bindings.
Pooling and Reset
Provider release stores the whole tree by style key and clears its layout replay key. It does not recreate children or restore arbitrary callback writes. Assume these values survive unless you reset them:
- child fields such as
translation,opacity,visible,content,control, and custom metadata - observers, subscriptions, timers, and delayed callbacks
- media playback and content nodes
- callback-owned animation state
- any status field not restored by the next overlay
Use two complementary reset points:
onDataChangeresets content-owned visual state before configuring a new item.onUnmountcancels timers, observers, network or media work, and restores callback-owned fields before pooling.
The same field-reset handler can be used for both callback groups:
export const resetCard: ViewFragmentCallbackHandler = (
_host,
fragment,
): void => {
fragment.viewsById.parallaxLayer.translation = [0, 0];
fragment.viewsById.veil.opacity = 0.22;
};
Resource-owning fragments need stronger teardown. For example, a video handler should cancel delayed playback, remove scoped observers, stop playback, hide the node, and clear its content. Perform equivalent cleanup before configuring a different item because a pooled instance can move directly between items.
Host Capability Matrix
| Capability | DynamicCell | FragmentView | Header fragment |
|---|---|---|---|
| Provider-created and pooled tree | Yes | Yes | Yes |
| Content source | CollectionView item | itemContent / updateData(...) | Row content.data during header configuration |
| Direct and function bindings | Yes | Yes | Yes, when data-map support and row data are present |
| Computed values and constraints | Yes | Yes | Yes, during supported header data application |
contentHeightAnchorViewId auto-height | Yes | Yes, with a constraint pass | No full auto-height pass |
| Status overlays | CollectionView status path | Explicit/focus-driven status path | Not applied |
| Lifecycle callbacks | Full | Full | Not invoked |
| Focus holder | Cell-managed fragment root | Opt-in fragment root | None |
Treat header fragments as presentational data-bound trees. If a header needs a
full callback/status lifecycle, mount a normal FragmentView or own the
interaction outside the header-fragment path.
Canonical Samples
- Registration and property functions:
hosanna-ui-samples-public/src/hosanna-ui-examples/ExampleApp.ts - Standalone order, data, status, and unmount counters:
hosanna-ui-samples-public/src/hosanna-ui-examples/rigs/views/FragmentViewRig.ts - Pooled-field reset:
hosanna-ui-samples-public/src/hosanna-ui-examples/rigs/wow-samples/LumenMaskedWorldsCallbacks.ts - Video timers, playback, content, and observer cleanup:
hosanna-ui-samples-public/src/hosanna-ui-examples/rigs/wow-samples/LumenVideoCellCallbacks.ts - Selected, focus-selected, and focus-footprint behavior:
hosanna-ui-samples-public/src/hosanna-ui-examples/rigs/collection-view/CellSelectedStatusRig.ts
Lifecycle Checklist
- Register callbacks before fragment acquisition or prewarming.
- Make
views.basea valid first normal appearance. - Restore every field changed by another reachable status.
- Keep descendant IDs stable and unique; omit a fixed reusable root ID.
- Do not assume data-bound child fields are ready in every
onMount. - Reset previous-item state in
onDataChange. - Cancel observers, timers, media, and external work in
onUnmount. - Do not make teardown depend on the previous item's data or status.
- Catch recoverable callback errors so essential cleanup can finish.
- Do not make header fragments depend on callbacks or status overlays.
- Repeat same-group callback arrays and concrete bindings in derived styles where required.
Prefer bindings, computed values, and constraints before adding imperative callbacks.