Skip to main content

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:

EnumAppConfig keyIntended use
ViewStatus.NoneNoneInternal sentinel; not normally an authored visual bucket.
ViewStatus.NormalnormalResting, enabled appearance.
ViewStatus.FocusedfocusedFocused but not selected.
ViewStatus.FocusFootprintfocusFootprintLast-focused footprint after active focus moves elsewhere.
ViewStatus.SelectedselectedSelected but not focused.
ViewStatus.DisableddisabledDisabled appearance.
ViewStatus.ErrorerrorValidation or error appearance.
ViewStatus.FocusSelectedfocusSelectedFocused 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. Use fragment.view for the root and cell for host geometry in constraints.
  • Do not give a child the same ID as the root; legacy findNode fallback 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;
ArgumentContract
hostOwning DynamicCell or FragmentView. Narrow it only for a deliberately host-specific style.
fragmentAcquired instance. Use fragment.view for its root and fragment.viewsById for descendants.
dataCurrent host content when available. Do not assume onUnmount receives the previously rendered item.
viewStatusCurrent 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

Shared fragment resolution followed by the different DynamicCell, FragmentView, and header lifecycle ordersShared fragment resolution followed by the different DynamicCell, FragmentView, and header lifecycle orders

DynamicCell

When a cell needs a different style or row container, the normal sequence is:

  1. invoke onUnmount on the old fragment
  2. release it
  3. acquire and append the new fragment
  4. invoke onMount
  5. apply direct and function data bindings
  6. evaluate computed values for the target status
  7. apply a changed status overlay
  8. apply constraints and invoke onApplyViewStatus
  9. 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 invokes onDataChange
  • a changed status evaluates computed values, applies the overlay, invokes onApplyViewStatus, and can reapply stored itemContent
  • recycle() and host unmount invoke onUnmount before 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:

  1. onDataChange resets content-owned visual state before configuring a new item.
  2. onUnmount cancels 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

DynamicCell and FragmentView support the full callback and status lifecycle while header fragments support only their data, computed-value, and constraint pathDynamicCell and FragmentView support the full callback and status lifecycle while header fragments support only their data, computed-value, and constraint path

CapabilityDynamicCellFragmentViewHeader fragment
Provider-created and pooled treeYesYesYes
Content sourceCollectionView itemitemContent / updateData(...)Row content.data during header configuration
Direct and function bindingsYesYesYes, when data-map support and row data are present
Computed values and constraintsYesYesYes, during supported header data application
contentHeightAnchorViewId auto-heightYesYes, with a constraint passNo full auto-height pass
Status overlaysCollectionView status pathExplicit/focus-driven status pathNot applied
Lifecycle callbacksFullFullNot invoked
Focus holderCell-managed fragment rootOpt-in fragment rootNone

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.base a 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.

Talk to us