A master-detail view needs a current selection, but that does not mean the selection belongs in shared State. Chapter 3 of the SDuX Vault tutorial adds an interactive read path while keeping the committed character collection behind its service boundary.
This distinction applies to pickers, record browsers, settings panels, and search-result views. The collection may be useful to multiple consumers, while the item currently being inspected is usually meaningful only to the view doing the inspecting.
Key takeaway: Keep committed Feature State in the feature service. Keep the current selection local to the view, then derive the selected record from both values.
From a Fixed Read View to a User-Selected Read Path
The previous chapter establishes a complete read path: a typed character collection is registered as Feature State, a service exposes access to it, and the component renders a character. The initial screen can use a fixed record to make the ownership boundary visible.
Chapter 3 changes the presentation path, not the ownership model. The view receives the same managed collection and adds a selection control. Selecting an item does not copy the collection into the component, create a second store, or ask the feature service to remember which row a reader opened. It records only the local choice and derives the detail record from the existing collection.
| Concern | Owner | Reason |
|---|---|---|
| Character collection | Feature service | Committed feature data that other consumers may read |
| Selected character ID | Displaying view | Temporary navigation in one presentation context |
| Selected character | Derived read path | Recalculated whenever the collection or selection changes |
The result is interaction without duplication. The view becomes more useful while the State contract remains understandable to every other consumer.
Shared Collection State vs Local Selection State
The key question is not whether a value changes. Both a collection and a selection can change. The question is whether it belongs to the feature’s committed domain State or is merely a temporary decision made by one view.
A character collection may be read by a list, detail panel, search result, and separate summary. It belongs behind the feature service because it is the shared source of truth. A selected ID in one master-detail screen does not automatically have meaning for those other consumers. Storing it centrally would couple them to a navigation choice they did not make.
Ownership test: If two independent consumers need the same committed value, it is a candidate for shared Feature State. If a value only tells one view what it is currently displaying, keep it local until a real shared requirement appears.
Local state still has a precise job: it is the input to the read path, not a replacement for the managed collection. A second detail panel can choose its own item without overwriting the first panel’s choice.
Deriving the Selected Record Reactively
Chapter 3 exposes the collection as a reactive value and keeps a nullable selected ID in the component. The selected record is a projection: find the record whose identifier matches the local choice, or return no record when there is no match.
The Angular implementation is:
protected readonly selectedCharacterId = signal<number | null>(null);
protected readonly selectedCharacter = computed(() => {
const selectedId = this.selectedCharacterId();
return this.characters().find(({ id }) => id === selectedId) ?? null;
});
Angular Signals provide the mechanics; the ownership rule is not Angular-specific. The implementation is presented as an example, not as a claim that every framework should use Signals. React, Vue, or Svelte would use that framework’s reactive primitive while preserving the same inputs and derived result.
The computed value does not fetch a second copy of the character, mutate the shared collection, or write the selected record back into Feature State. It joins a local presentation input with shared committed data and produces the value required by the detail panel.
If the service-owned collection changes, the lookup runs again. If the local selection changes, it runs again. The detail panel reflects the latest valid combination instead of a stale object copied during an event handler.
Handling Empty and Unknown Selections
An interactive view has more states than “the record is visible.” The collection may still be empty while the feature initializes. A user may not have selected anything. A stale link, malformed control value, or collection refresh may refer to an identifier that is no longer present.
Chapter 3 treats these as normal read-path outcomes. The selected record is nullable, and the template displays an empty state when the lookup returns no match. The selection handler ignores an unknown value instead of placing invalid data into local state.
| Condition | Derived result | View response |
|---|---|---|
| No records loaded | No selected record | Disable or leave the picker empty and explain the next step |
| No selection yet | null |
Show “No character selected” rather than inventing a default |
| Unknown identifier | No selected record | Ignore the invalid choice and preserve a safe empty state |
| Valid identifier | Matching committed record | Render detail fields from the derived value |
⚠️ Warning: Do not use a default record to hide an invalid state. A default can be a deliberate product decision, but silently displaying the first record makes an empty or stale selection look valid and makes the interaction harder to reason about.
Testing Interaction Without Moving State Ownership
The ownership boundary gives the tests a straightforward shape. The feature-service test verifies that the managed collection is available through its committed State path. The view test verifies that selection changes the derived detail record, that the empty state appears before a valid choice, and that an unknown identifier does not produce a fabricated detail view.
| Test focus | Useful assertion |
|---|---|
| Initial view | Detail panel shows the empty state and the picker is safe to use |
| Valid selection | Detail fields match the selected record in the shared collection |
| Unknown selection | No invalid record is rendered and the view remains predictable |
| Collection refresh | Derived value reflects the newest collection for the local ID |
These are separate assertions because they protect separate responsibilities. A view test should not inspect feature-service internals, and a service test should not render a detail panel merely to prove the collection exists.
The same tests apply in any UI framework. Subscription, memoization, and rendering syntax may change, but the behavior contract remains: shared data remains shared, temporary navigation remains local, and the detail view is derived from a valid pair of inputs.
A Boundary That Scales Beyond the Tutorial
Chapter 3 demonstrates how to add interaction without broadening State ownership. The service keeps the feature’s committed collection available to every consumer, while each view decides what it is currently reading.
If a later requirement says multiple screens must share the same selection, that is a new domain decision. Move it deliberately into shared Feature State only when the requirement is real and the selection has meaning beyond one view. Until then, local state is the smaller and clearer boundary.
The complete tutorial shows the interactive read path in context. Regardless of framework, the separation between committed Feature State and local presentation state remains constant.
Continue Learning
Remember: Share the collection because it is feature data. Keep the selection local because it is a view decision. Derive the detail record instead of duplicating it.
Top comments (0)