Skip to the solution
HardFrontend10 min read

What is NgRx?

675 views

The problem

Question presented to candidate: "Our team is debating whether to add NgRx to a growing Angular app. Explain what NgRx is, walk me through how data flows through it, and tell me when you would and would not use it."

What a strong answer should cover:

  • NgRx is a family of reactive state-management libraries for Angular; @ngrx/store implements the Redux pattern with RxJS and signals.
  • The building blocks: actions (named events), reducers (pure functions (state, action) => newState), the store (one immutable state tree), selectors (memoized queries), and effects for side effects such as HTTP.
  • Unidirectional data flow: component dispatches, reducer computes, selector reads; no component mutates shared state directly.
  • Benefits: predictability, Redux DevTools time-travel debugging, testable pure functions, clear separation of concerns. Costs: boilerplate and indirection, which createActionGroup / createFeature reduce.
  • Modern details: provideStore() / provideState() for standalone apps, store.selectSignal(), runtime immutability checks in dev mode, and NgRx SignalStore (@ngrx/signals) as the lighter, signal-based option.
  • When not to use it: small apps or local UI state, where a service with signals is enough.

Clarifying questions expected:

  • "How much state is shared between distant parts of the app, and how many developers touch it?"
  • "Do we need features such as undo, time-travel debugging or strict auditing of state changes?"

Code / implementation expected: Optional β€” a small feature with actions, a reducer, a selector and a component dispatching and reading.

  • ngrx
  • state

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 11 sections

Read the code

AngularA cart with createActionGroup, createFeature, selectors and store.selectSignal (NgRx 22)
import { ApplicationConfig, Component, inject, signal } from "@angular/core";
import { ActionReducer, Store, createActionGroup, createFeature, createReducer, createSelector, emptyProps, on, props, provideStore } from "@ngrx/store";

// 1. Actions: named events. createActionGroup builds "[Cart] Add Item" etc.
export const CartActions = createActionGroup({
  source: "Cart",
  events: {
    "Add Item": props<{ name: string; price: number }>(),
    "Remove Last": emptyProps(),
    Clear: emptyProps(),
  },
});

// 2. State + reducer: a pure function (state, action) => new state. Never mutate.
interface CartState {
  items: { name: string; price: number }[];
}
const initialState: CartState = { items: [] };

export const cartFeature = createFeature({
  name: "cart",
  reducer: createReducer(
    initialState,
    on(CartActions.addItem, (state, { name, price }) => ({ ...state, items: [...state.items, { name, price }] })),
    on(CartActions.removeLast, (state) => ({ ...state, items: state.items.slice(0, -1) })),
    on(CartActions.clear, () => initialState),
  ),
  // 3. Selectors: createFeature generates selectCartState and selectItems; we add derived ones.
  extraSelectors: ({ selectItems }) => ({
    selectCount: createSelector(selectItems, (items) => items.length),
    selectTotal: createSelector(selectItems, (items) => items.reduce((sum, i) => sum + i.price, 0)),
  }),
});

// A meta-reducer sees every action before the reducers: handy for logging.
export const actionLog = signal<string[]>([]);
function logger(reducer: ActionReducer<any>): ActionReducer<any> {
  return (state, action) => {
    actionLog.update((log) => [...log, action.type]);
    return reducer(state, action);
  };
}

export const appConfig: ApplicationConfig = {
  providers: [provideStore({ [cartFeature.name]: cartFeature.reducer }, { metaReducers: [logger] })],
};

@Component({
  selector: "app-root",
  template: `
    <button (click)="add('Book', 12)">Add book</button>
    <button (click)="add('Pen', 3)">Add pen</button>
    <button (click)="store.dispatch(actions.removeLast())">Remove last</button>
    <button (click)="store.dispatch(actions.clear())">Clear</button>
    <p>{{ count() }} items, total {{ total() }}</p>
    <ul>
      @for (item of items(); track $index) {
        <li>{{ item.name }} ({{ item.price }})</li>
      }
    </ul>
    <p>Actions seen: {{ log().join(" > ") }}</p>
  `,
})
export class AppComponent {
  store = inject(Store);
  actions = CartActions;
  log = actionLog;
  // selectSignal turns a selector into an Angular signal the template can read.
  items = this.store.selectSignal(cartFeature.selectItems);
  count = this.store.selectSignal(cartFeature.selectCount);
  total = this.store.selectSignal(cartFeature.selectTotal);

  add(name: string, price: number) {
    this.store.dispatch(CartActions.addItem({ name, price }));
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 98 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextHard

Explain the shareReplay operator in RxJS and how it prevents duplicate HTTP requests.

Continue