What changes when the same feature receives its data four different ways: during hydration, from a Promise, through an Observable, and from an Angular HTTP Resource? The source contracts look different enough to invite separate loading, error, and state-management paths. This chapter follows each one to the same service-owned FeatureCell™ boundary, where the more important question is revealed: who still owns the result when the source succeeds, stalls, or fails?
The component supplies teaching controls and presentation state. The service registers the source and chooses the appropriate state API. The pipeline resolves the input, applies the existing filters and reducers, tracks loading and errors, and commits the resulting State. The source changes; the ownership boundary does not.
Key takeaway: Begin with the completed Chapter 7 project. Chapter 9 extends the same Chapter 7 character workflow and service-owned pipeline rather than replacing it with a new asynchronous architecture.
Continue the Chapter 7 Pipeline Through an Async Lab
Chapter 7 established the successful data path: a candidate enters the service-owned pipeline, the pure filter removes the teaching record whose last name is unknown, and ordered reducers derive display fields and sort the retained collection. Chapter 8 kept that path intact while making failure observable through a controlled filter error.
Chapter 9 keeps both lessons and changes the input timing. The completed Chapter 8 project becomes the starting point for a lab with four source contracts. Hydration controls initialization. Promise and Observable examples submit later updates through mergeState(). HTTP Resource submits a validated replacement through replaceState().
| Input | Boundary | Lab result |
|---|---|---|
| Hydration Promise | initialize() |
Supplies the authoritative initial collection before the first committed State. |
| Promise | mergeState() |
Resolves once, removes Grogu through the existing filter, and appends Ahsoka and Din. |
| Observable | mergeState() |
Uses the first emitted collection, removes R2-D2, and appends Ezra and Hera. |
| HTTP Resource (Angular only) | replaceState() |
Parses the selected remote characters, removes Yoda, and replaces the prior collection with Lando and Han. |
⚠️ Warning: The controlled Promise and Observable helpers expose a pending interval so you can see loading and failure. Settle the current source before starting another; do not infer a general policy for cancellation, repeated emissions, or overlapping requests from this lab.
Hydrate Authoritative Initial State
Hydration is different from a later fetch because it controls the first initialization request. The service registers a deferred hydrate() factory before it calls initialize(). The factory does not run when it is registered; initialization waits for it to resolve or reject.
Resolve the teaching Promise and the hydrated collection continues through the same Resolve, Filter, Reducer, and Emit stages already used by the feature. Reject it and initialization enters Error State. The configured initialState is not silently used as a fallback, and persistence does not replace the authoritative hydration result.
This is why the lab keeps the Resolve and Reject buttons separate from the service. The component decides when to settle the teaching helper, but the FeatureCell owns initialization, loading, error handling, and the eventual State snapshot.
Key takeaway: Reload the example and leave hydration pending. The loading indicator remains active until you choose Resolve or Reject. That interval belongs to the FeatureCell lifecycle, not to a second loading flag invented by the component.
Resolve Promise and Observable Inputs Through mergeState()
After initialization, a Promise and an Observable represent two different ways to deliver a later candidate. Both can be submitted through mergeState(), but the source contracts are different: a Promise resolves once, while the Observable Resolve behavior subscribes and extracts a single emitted value for downstream processing. The component starts the request and exposes the teaching control; it does not await the Promise or subscribe to the Observable.
Promise: await one deferred result
The Promise helper holds one pending Promise. When you click Resolve, Ahsoka, Din, and Grogu become the resolved collection. The existing filter removes Grogu, the reducers derive the display fields, and array-append merge keeps the existing collection while adding the retained characters. When you click Reject, the previously committed collection remains visible and the error lifecycle completes.
const deferredPromise = examplePromise.getPromise();
characterCell.mergeState({
value: () => deferredPromise
});
The request shape is intentionally small. The service passes the deferred source to the FeatureCell; the pipeline owns waiting, transformation, error handling, and commitment. A rejected Promise does not erase the last committed collection.
Observable: resolve one emitted value
The Observable helper uses a controlled ReplaySubject. Click Add by Observable to start the request, then Emit or Error to settle it. The Observable Resolve behavior owns the subscription and extracts one emitted collection for downstream processing. This is a single state update, not a continuous stream subscription owned by the component.
import { of } from 'rxjs';
const characters$ = of([
{ id: 201 },
{ id: 202 },
{ id: 203 }
]);
characterCell.mergeState(characters$);
The Observable source is submitted to the FeatureCell just like the Promise source, but Resolve handles the Observable subscription and forwards its single emitted value. The existing filter, reducers, and array-append merge then process that value. In the lab, Emit appends Ezra and Hera after R2-D2 is filtered out; Error preserves the previously committed collection while the pipeline error lifecycle completes.
⚠️ Warning: Do not move the subscription into the component. Doing so would create a second lifecycle for loading, errors, and cleanup. Submit the Observable to the service-owned boundary and let the Resolve behavior handle the single emitted value.
Replace State from a Validated HTTP Resource (Angular Only)
The HTTP Resource example uses a different write promise. The service creates a resource through the teaching adapter and passes it to replaceState(). The component starts the request but never reads the resource directly.
The adapter calls the public SWAPI people endpoint, selects the characters needed by the tutorial, derives numeric identities from canonical resource URLs, and rejects incomplete data before it reaches State. The response is untrusted at this boundary, so parsing and validation stay with the transport adapter.
On success, the selected collection continues through the existing Filter and Reducer stages before it replaces the previous collection. Yoda is removed because its last name is unknown, leaving Lando and Han in last-name order. On transport, parsing, validation, or HTTP failure, the previous collection remains committed and the normalized error is visible.
this.#vault.replaceState(
exampleHttpResource.getResource(this.#injector)
);
⚠️ Warning: Connectivity, CORS policy, rate limits, and remote schema changes can make SWAPI unavailable. Treat a normalized failure with the old collection preserved as correct boundary behavior. Do not weaken response validation to force an external response to commit.
Compare Four Async Source Contracts
The four examples differ in when the source begins and whether the request merges or replaces State. They do not require four copies of the application's loading, error, filtering, and reduction logic.
| Source | Successful path | Failure path |
|---|---|---|
| Hydration | Authoritative initial data is filtered and reduced before initialization commits. | Initialization enters Error State instead of falling back to configured initial State. |
| Promise | One resolved collection is filtered, reduced, and appended. | The existing collection remains visible while the rejection becomes Error State. |
| Observable | The first emitted collection is resolved, filtered, reduced, and appended. | The existing collection remains visible while the source error is normalized. |
| HTTP Resource (Angular only) | A parsed and validated collection is filtered, reduced, and committed as a replacement. | Transport or validation failure preserves the prior collection. |
The execution guarantee is the same across these inputs: the pipeline computes the outcome before State commitment. Observers do not see a partially parsed response, an intermediate reducer result, or a collection that was changed before a later async failure was known. Each successful input produces one finalized snapshot, while a failed input leaves the previous committed collection intact when the request is a later merge or replacement.
Key takeaway: Choose the state API that matches the source contract, then keep resolution, transformation, loading, errors, and commitment inside the same FeatureCell pipeline.
Verify Loading, Errors, and Service-Owned Commitment
Run the lab as a sequence of observable contracts. First reload and resolve hydration. Confirm that the hydrated collection commits only after the deferred source settles, that BB-8 is removed, and that reducers add display fields and sort the result. Repeat with Reject and confirm that initialization ends in Error State without silently using the configured initial collection.
Next run the Promise and Observable paths. Watch state.isLoading() while each source is pending. Resolve or Emit and confirm the retained characters are appended. Reject or Error and confirm the committed collection is preserved. The component's buttons make the timing visible, but the service remains the only owner of the source-to-State transition.
Finally run the HTTP Resource path. Confirm that a valid response replaces the collection with Lando and Han after parsing, validation, filtering, and reduction. If the public endpoint fails, confirm that loading ends, the normalized error is visible, and the previous collection remains intact.
This lab is complete when you can explain the difference between initialization-time hydration and later asynchronous updates, and when all four sources show the same FeatureCell loading and error lifecycle without moving pipeline authority into the component.
Deeper Dive
Continue with the complete Chapter 9 lab, then review the HTTP Resource Resolve behavior, Promise Resolve behavior, Observable Resolve behavior, and the pipeline execution guarantees.
StackBlitz demo: Chapter 9 — Route Async Inputs Through One Service-Owned Pipeline.
Top comments (0)