cswork / libraryOpen practice workspace

Articles

NgRx Interview Questions: Effect Lifecycles, Selector Memoization, and Entity Updates

Practice NgRx interview questions with an orders trace covering effect error lifecycles, selector memoization, and partial entity updates.

A strong NgRx interview answer follows an action through three separate jobs: the effect handles external work, the reducer updates entity state, and a selector derives the view. A failed HTTP call should not silently stop future actions. A selector should not recompute merely because an unrelated field changed. An entity update must preserve the normalized collection's identity rules. Those are observable outcomes you can trace, not slogans to memorize.

This article uses an original orders example for practice, not a report of a particular employer's interview. NgRx's effects guide, selector guide, and entity-adapter guide support the framework behavior. The event traces and interview prompts below are editorial exercises. For a related Angular debugging prompt, open Debug an Angular UI from User Reports and explain which observation would distinguish a stopped effect from a stale selector.

Interview question: why did the effect stop after one failed request?

An effect is a long-lived listener of dispatched actions. Consider an orders screen that dispatches loadOrders every time a user opens or refreshes a list. The effect calls an API, then emits either loadOrdersSuccess or loadOrdersFailure. The reducer handles those actions. An effect does not directly mutate the store: it emits an action that the reducer can process.

The critical boundary is where the error is caught. In this schematic TypeScript, the HTTP observable is inside exhaustMap, and catchError is inside that inner observable:

loadOrders$ = createEffect(() =>
  actions$.pipe(
    ofType(OrdersPageActions.loadOrders),
    exhaustMap(() =>
      api.getOrders().pipe(
        map(orders => OrdersApiActions.loadOrdersSuccess({ orders })),
        catchError(error =>
          of(OrdersApiActions.loadOrdersFailure({
            message: readableMessage(error)
          }))
        )
      )
    )
  )
);

On a failed request, the inner call emits a failure action and finishes; the outer action listener remains available for the next loadOrders. NgRx's error-handling example puts catchError inside a flattening operator for this reason. If the inner error escapes to the outer effect stream, that stream can terminate or enter NgRx's effect error-handling path. Do not claim that NgRx will always retry your particular HTTP request: framework resubscription to an effect source is a different mechanism from an application-level network retry, and exact behavior depends on configuration and version.

Trace two actions to test your explanation. At t1, loadOrders produces an HTTP 503 and loadOrdersFailure. At t2, a user clicks Retry, dispatching loadOrders again. A healthy effect sends the second request and can emit success. A broken effect with misplaced error handling may never observe t2. The assertion that matters is two invocations and a success after the first failure, not merely that the UI displays an error after t1.

Which RxJS flattening operator matches the user action?

The correct operator follows the product contract. exhaustMap ignores a second load action while a request is active, which can suit a button that should not duplicate an in-flight load. switchMap switches to the latest inner observable, useful for a search term where earlier results become stale. concatMap queues actions in order when every operation must finish sequentially. mergeMap permits overlap when independent operations may proceed concurrently. NgRx's effects guide names these operators as ways to keep inner work and errors inside a long-lived action listener.

Do not infer that unsubscribing a client observable cancels a server-side mutation. If a save request reached the server before switchMap moved to a newer action, the first save might still complete. For writes, define whether both writes must be preserved, serialized, deduplicated, or rejected. For reads, define whether only the latest response may update state. A strong interview answer states the policy before choosing an operator.

There is another lifecycle trap: a side-effect-only effect must declare dispatch: false if it intentionally emits no action. Otherwise, explain what action it emits and whether an accidental loop could arise. NgRx's examples use non-dispatching effects for work such as an alert. The observable's lifetime, returned action, and reducer transition are separate questions; trace each one.

Interview question: when does a selector recompute?

A selector is a derived read, not a second place to store data. Suppose the normalized feature state contains ids, entities, selectedId, and loading. The screen selects all orders, the selected order, and a visible count. NgRx memoizes selector projectors so a projector can reuse its previous result when its selected input values are unchanged. The selector documentation explains composition and memoization; the defaultMemoize API exposes the equality hooks behind the default mechanism.

Here is the original trace to speak through. On the initial selection, selectEntities returns reference A and a derived projector runs. Then loading changes, but entities remains reference A. A selector whose inputs are only entities can reuse its result; a selector that takes the entire feature-state object as input may see a changed object and run again. Then updateOne creates a new entities reference B for an order change, so the relevant projector runs. The supporting visual records A/A/B rather than asserting that every action causes a recomputation.

Action observedSelected inputExpected projector behavior
Initial readentities reference ARun to compute the first result
Unrelated loading updateThe same reference AReuse the memoized result for this selector
Entity record updateA new reference BRun again for the changed input

That prediction is conditional on the selector's actual inputs and equality function. A selector that also takes loading will run on the second row. A selector recreated on every component call may lose the cache you expected. Mutating an entity in place can leave a reference unchanged and create stale-looking output. Rather than announcing “NgRx selectors are fast,” inspect the input selectors, result references, and projector invocation count in a focused test.

If the interview asks about selecting one order by ID, prefer a stable, well-scoped selector or documented factory pattern. Avoid building a fresh projector in a template expression on every change-detection pass. For an expensive list calculation, first ask whether unrelated state is included as an input and whether the reducer preserves references for unchanged data. Memoization is an input-identity contract, not a substitute for correct state ownership.

Interview question: what does updateOne actually change?

NgRx Entity stores a collection with an ids array and an entities dictionary. That shape offers lookup by key and selectors for IDs, entities, all rows, and total. The Entity adapter reference distinguishes addOne, setOne, updateOne, and upsertOne. In particular, updateOne takes an ID plus partial changes. upsertOne is an add-or-replace operation; do not describe it as the same partial patch.

Consider an order { id: 'o7', status: 'pending', total: 40 }. A success response contains only { id: 'o7', changes: { status: 'paid' } }. Using adapter.updateOne updates the status while retaining the other fields. Replacing the whole record with only the patch fields would accidentally lose total. Conversely, if the API returns a complete authoritative order, setOne may better express replacement. The choice depends on what the API response means, not merely on which helper compiles.

selectId defines identity. If the server changes an entity's identifying field, plan that change explicitly: clients, selection, and references to the old ID may all need reconciliation. A sorted adapter's sortComparer can also affect ordering when relevant fields change. Do not promise the ids array always stays in the same order after an update; state the adapter configuration. The feature-creator recipe shows how adapter selectors can be connected to a feature selector.

A useful reducer trace starts with two IDs, o7 and o9. Apply a paid-status patch to o7. Verify entities.o7.status is paid, entities.o7.total is still 40, entities.o9 is unchanged, and selectors show the intended list. Then dispatch an unrelated loading action and inspect whether the derived selector's inputs changed. This combines reducer correctness with memoization instead of testing each helper in isolation.

For a real incident, also check whether the backend sent a partial patch or a complete snapshot. An updateOne test built only from complete objects may never reveal a lost-field bug; a setOne test built only from partial responses can normalize incomplete records into the store. Add one assertion about a field that was not included in the response. That assertion is the quickest way to show whether the update helper matches the API contract.

How do you debug “the UI is stale” without guessing?

Start with an observable trace. Did the click dispatch an action? Did the effect see it? Was the HTTP call sent and did it return? Which success or failure action was emitted? Did the reducer produce a new relevant state reference? Did the component select from the correct feature? If the first missing step is the second HTTP call after a failure, investigate the effect lifetime. If actions and reducer state are correct but the view is stale, inspect selector inputs, accidental mutation, and subscription or change-detection boundaries.

Use a small failure matrix rather than changing several layers at once. If loadOrdersFailure arrives, the API error path works; next verify the subsequent loadOrders reaches the effect. If loadOrdersSuccess arrives but entities never changes, the reducer or action payload is suspect. If entities changes but a derived view does not, check selector wiring and input identity. Each observation eliminates a class of causes.

QuestionNgRx connectionFollow-up to answer aloud
Debug an Angular UI from User ReportsAction-to-view traceWhich layer first diverged from the user's observation?
Implement Resilient Autocomplete Data FetchingSwitch/cancel/stale resultWhich response may update state after a new query?
Design State for a Three-Column Work BoardNormalized entity ownershipWhat is the stable ID and what is derived?
Build a Real-Time Event Feed DashboardLong-lived event effectsHow do reconnects and errors preserve the listener?
Diagnose Cold-Load and Massive-Table Performance in a Web AppSelector costWhich measured calculation actually reruns?

A compact interview answer is: “I trace the action, the inner async operation, the success or failure action, the reducer transition, and the selector inputs. I catch a request error inside the flattened observable so later actions still reach the effect. I choose switchMap, concatMap, mergeMap, or exhaustMap from the operation's concurrency contract. Then I check selected input references before claiming memoization helped, and I use the entity adapter method that matches partial patch versus full replacement.” If the interviewer changes the requirement from read to write, or from one order to a sorted collection, revise the operator and adapter answer accordingly.

For another concrete exercise, start with Debug an Angular UI from User Reports, draw the action/effect/reducer/selector trace before reading its approach, and explain the one observation that would falsify your first hypothesis.

Sources and Further Reading