Repeated inputs are normal. The difficult question is not whether a candidate looks similar to the last one, but what your domain means by “unchanged.” Chapter 14 uses the Operator stage and withDistinctUntilChanged to suppress candidates whose character IDs are already present before downstream filters and reducers run.

The example keeps the comparison function in the service-owned FeatureCell™ pipeline. The component exposes buttons and observes the result; it does not decide whether a candidate is distinct. That division makes the behavior easy to demonstrate and its production limitation impossible to miss.

Core idea: distinctness is a business rule with a specific scope. A comparator that only checks identity can be useful for one input path, but it is unsafe as a blanket rule for creates, edits, removals, or ordered collections.

Why Repeated Inputs Need a Meaning of “Unchanged”

Applications receive repeated candidates for many ordinary reasons: a user clicks submit twice, a refresh returns an already-known record, or an upstream source replays its latest value. Treating every repeated candidate as meaningful can send unnecessary work through the rest of the pipeline. But “unnecessary” depends on what the feature is trying to protect.

For the Chapter 14 exercise, the narrow question is whether the incoming character collection contains at least one identity that was not already present. That is different from asking whether two arrays are deeply equal. It is also different from asking whether every field, array position, or deletion has the same meaning to the application.

Question Why the answer matters
Is this identity new? A new record may need to continue through merge and downstream behavior.
Did an existing record change? Field edits can be valid work even when the identity is familiar.
Was an identity removed? A removal-only candidate may be meaningful without introducing a new ID.
Does order carry meaning? An identity set can hide a reorder when position is part of the feature.

Register Distinct Until Changed at the Operator Stage

The Operator stage runs after Resolve and Merge, then before Filters and Reducers. That placement matters: the distinctness decision can stop a redundant candidate before later transformation work runs. The service owns this configuration alongside its other pipeline behaviors, so every caller gets the same rule.

The source service registers the operator with an array, just like its filters and reducers. The callback receives the incoming value and the previous value, then compares character identities.

import { withDistinctUntilChanged } from '@sdux-vault/addons'; import { FeatureCell } from '@sdux-vault/core'; import type { StarWarsCharacter } from './star-wars-character.shape'; export const characterCell = FeatureCell<readonly StarWarsCharacter[]>({ key: 'star-wars-character', initialState: [] }); characterCell .operators([ withDistinctUntilChanged<readonly StarWarsCharacter[]>( (incoming, previous) => incoming.every(({ id }) => previous.some((character) => character.id === id) ) ) ]) .initialize(); 

Scope the rule deliberately: the callback above is an identity-set comparison. It is not a general deep-equality comparator and should not be copied into every CRUD pipeline without checking what edits, removals, and ordering mean.

How the Identity-Set Comparator Suppresses Candidates

Read the callback as a set-membership test. For every incoming character, it looks for a previous character with the same ID. If all incoming IDs are already represented, the candidate is treated as unchanged by this rule. If the incoming collection contains a new ID, the comparison returns a different result and the candidate remains eligible for the later pipeline stages.

The tutorial makes the result observable with two service methods. submitSameState() submits the same character identity, while submitChangedState() advances to a candidate that changes the identity set. The component delegates both actions to the service and keeps its role focused on controls and observation.

const DISTINCT_SAME_STATE_CHARACTER: StarWarsCharacter = { id: 501, name: 'Rey', lastName: 'Skywalker', faction: 'Jedi Order', isForceSensitive: true }; let distinctChangedStateIndex = 0; export function submitSameState(): void { characterCell.mergeState([{ ...DISTINCT_SAME_STATE_CHARACTER }]); } export function submitChangedState(): void { const character: StarWarsCharacter = { id: 502 + distinctChangedStateIndex, name: 'Luke', lastName: 'Skywalker', faction: 'Jedi Order', isForceSensitive: true }; distinctChangedStateIndex += 1; characterCell.mergeState([character]); } 

In the demo, a repeated candidate leaves the observed value unchanged. A changed candidate can continue to the rest of the pipeline. The distinction is visible without making the component aware of comparison internals.

Why a Narrow Comparator Is Unsafe for General CRUD

The same comparator can silently hide valid work. If a user edits Leia’s faction but keeps her ID, every incoming ID may still be present in the previous value. The identity-set rule can therefore suppress an edit. A removal-only candidate has the same problem: it introduces no new ID, even though the collection should lose a record. Reordering also disappears if the feature treats array position as meaningful.

Candidate change What the narrow rule can do
Edit an existing record Suppress it because the edited ID already exists.
Remove a record Suppress it because no new ID appears.
Reorder records Ignore the change because membership is unchanged.
Add a new record Allow it because an incoming ID is not in the previous set.

Production check: choose a comparator from the feature’s semantics. If updates, deletes, ordering, or derived values matter, compare those dimensions or avoid this narrow identity-only rule for that input path.

Observe Reset and Suppressed-Request Behavior

The cleanest way to study distinctness is to observe the resulting State rather than adding control logic to the component. Press the same-state control, wait for the pipeline to settle, and confirm that the observed collection remains unchanged. Then submit a changed candidate and confirm that the expected value appears.

The chapter’s tests use the same sequence: act, await vaultSettled(key), then assert. That ordering keeps the test focused on the public outcome. Add cases for edits, removals, and ordering before adopting the rule in a feature with broader CRUD semantics.

Reset behavior is an equally useful boundary check. A reset is a deliberate lifecycle operation, not proof that every ordinary candidate is distinct. Verify that your feature’s reset contract is tested separately from the identity comparison, especially when a previously known collection can be intentionally replaced.

Try the Chapter 14 distinct-until-changed tutorial to compare the same-state and changed-state controls in the FeatureCell™ service.

Deeper Dive

Work through the tutorial and review the comparator against your own domain rules before using it for CRUD. The useful question is not “Can this candidate be suppressed?” but “What work would become invisible if it were?”