Derive a prop from named inputs
Use a graph-derived store when the value belongs to the model. Use mapProps when the value belongs to this view and also needs incoming props. The projection receives resolved source values and the bound/incoming props. It should return a value without launching events or effects.
import {createElement as h} from "@fiberfx/hyperscript";
import {store} from "@fiberfx/hypergraph";
import {reflect} from "@fiberfx/reflect";
const $name = store("Ada");
const $count = store(2);
const LabelView = (p: {prefix: string; text?: string}) =>
h("p", null, p.text ?? "No label");
export const Label = reflect({
view: LabelView,
bind: {prefix: "Account"},
mapProps: {
text: {
source: {name: $name, count: $count},
fn: ({name, count}, props) => `${props.prefix}: ${name} (${count})`,
},
},
});
// h(Label, {}) -> Account: Ada (2)
// h(Label, {prefix: "Member"}) -> Member: Ada (2)
// h(Label, {text: "Fixed"}) -> Fixed
// h(Label, {text: undefined}) -> No label
The source may be one store, a flat object of stores, a tuple of stores, or {} when only incoming props matter. For example, source: [$name, $count] as const supplies a tuple to fn; it does not supply two Store objects.
Every projection sees the same base props. One projection cannot depend on another projection's output. Compute related values together in the model, or derive them independently from their real sources.
Change the source and the caller
Open the projection experiment. Change the name and prefix, then supply an own label override. Continue changing the source while the override is active. Finally try the explicit-undefined override.
Predict the displayed label before each action. Keep omission and explicit undefined separate: omission requests the default; an own key with undefined replaces it.
Observe a skipped projection
Without an override, changing the source or prefix should change the derived label. With a fixed override, the view should receive that fixed text. With explicit undefined, it should receive undefined and use its own empty-state rendering.
Reflect checks Object.hasOwn(props, key) before calling that projection. It therefore skips the overridden fn; it does not derive first and merely hide the result. A demonstration counter can expose evaluation, but production projections should remain pure. Skipping a projection does not promise that all subscriptions or rerenders disappear.
Reject ambiguous ownership
Do not place text in both bind and mapProps. Both types and runtime validation reject that conflict. A nested source such as {account: {name: $name}} is also unsupported: every direct member of a source object must be a store.
A returned function is an ordinary value, not an automatically scoped action. Do not move an event handler from bind into mapProps and assume its invocation remains scoped. Component replacement uses this distinction deliberately.
Restore the default path
Remove the own override, then reset the experiment. Change the prefix again and check that derivation is active. In application code, remove the key rather than spreading {text: undefined} when you want the default back.
Keep override state separate from source state so you can test both independently. A missing value and an intentional empty value often represent different user decisions.
Derive a useful status line
Create a status view with a caller-provided noun and a store-backed count: “3 tasks” or “3 messages”. Add a caller override for a maintenance notice. Check omission, fixed text and explicit undefined separately.
If you annotate the binding, prefer satisfies BindProps<Props> to preserve which literal keys are definitely supplied. An optional key in a broad binding type does not guarantee a required view prop. The next chapter places this derivation inside two independent scopes.