Treat a constructor as a value
Functions play different roles in Reflect. A function in bind or mapItem is wrapped as a scoped callback. A function returned by mapProps remains the exact value returned. A component constructor, class constructor or identity-sensitive callback ref needs the latter behavior.
import {createElement as h, type ComponentType} from "@fiberfx/hyperscript";
import {useState} from "@fiberfx/hyperscript/hooks";
import {store} from "@fiberfx/hypergraph";
import {reflect} from "@fiberfx/reflect";
const Compact = () => {
const [count, setCount] = useState(0);
return h("button", {onClick: () => setCount(n => n + 1)}, `Compact: ${count}`);
};
const Detailed = () => {
const [count, setCount] = useState(0);
return h("section", null, h("h3", null, "Details"),
h("button", {onClick: () => setCount(n => n + 1)}, `Detailed: ${count}`));
};
const Unknown = () => h("p", null, "Unknown component");
const registry = {compact: Compact, detailed: Detailed};
const $kind = store<string>("compact");
function selectView(kind: string): ComponentType<{}> {
return Object.hasOwn(registry, kind)
? registry[kind as keyof typeof registry] : Unknown;
}
export const Host = reflect({
view: ({View}: {View: ComponentType<{}>}) => h(View, {}),
bind: {},
mapProps: {View: {source: $kind, fn: selectView}},
});
Declare the registry and constructors outside render. Storing an identifier keeps model data serializable; constructors stay in application code. Do not return () => h(Compact) from the projection: that creates a new constructor on each evaluation and can remount the child.
For a per-row choice, wrap the row data with mapItem: {item: item => item} and derive RowView with source: {} from props.item.kind. The empty source means the projection depends on incoming props. It is not a special slot descriptor.
Replace a component intentionally
Open the component experiment. Increment Compact's local counter twice, then choose Detailed. Increment Detailed once, then return to Compact. Finally choose the unknown identifier.
Before each switch, predict whether the number should survive. These numbers are local component state, not a store in the shared model.
Distinguish a rerender from replacement
Changing Compact to Detailed should begin at zero. Returning to Compact should also begin at zero: the earlier Compact instance was removed, not parked for reuse. Selecting the unknown identifier should render the explicit fallback.
In a keyed list, preserving the row key is only part of the identity story. The same selected child constructor preserves its local state through row updates and reorder. A different child constructor intentionally replaces that child even when the row key stays the same. If data must survive that switch, give it a longer-lived owner in the model.
Keep callables in the right channel
Do not put an identity-sensitive constructor in bind or mapItem: those paths wrap raw functions. Do not assume a projected event or effect is scoped. A projection preserves that unit's identity and methods, but invocation still needs bind, the rendered child's useUnit, or an explicit scope binding.
Validate registry identifiers with an own-property check. constructor and __proto__ are not valid choices merely because a JavaScript object has a prototype. The lab chooses a fallback; an application may explicitly reject the value instead. Never silently pick an inherited member.
Restart from one constructor
Restart the exercise, confirm Compact starts at zero, and repeat the switch after a single click. Read the running source disclosure and locate the registry outside the component bodies.
For a separate identity test, hold a callback ref stable in a registry, return it through mapProps, and assert it is not detached simply because a keyed row moves. Changing the actual child constructor should detach the old ref and attach the new child.
Offer a compact and detailed row
Build two views with the same item prop and select between them by a model identifier. Keep selection data that must survive the switch in the model. Leave transient presentation state in the child only when resetting it is acceptable.
A projected child reads context where it renders. Its own useUnit can therefore use a nested Provider, while an already-bound callback passed from outside retains its original scope. No named-slot or preset API is required for this composition.