What is NgRx?
NgRx is a state management library for Angular built on the Redux pattern and RxJS. All shared state lives in one immutable store, and the only way to change it is to dispatch an action that a pure reducer handles.
35 questions - Intermediate to advanced
NgRx questions are really questions about the Redux pattern applied to Angular. Interviewers want to know that you understand one-way data flow, that reducers must be pure, that side effects live in effects, and - just as important - that you know when NgRx is overkill.
NgRx is a state management library for Angular built on the Redux pattern and RxJS. All shared state lives in one immutable store, and the only way to change it is to dispatch an action that a pure reducer handles.
In a large app, the same data is needed in many unrelated components, and passing it around or duplicating it in services leads to screens that disagree with each other. NgRx gives one source of truth, a predictable update path and a full history of what happened.
Store holds the state. Actions describe events. Reducers are pure functions that produce the next state. Selectors read slices of state. Effects handle side effects like HTTP and then dispatch more actions.
A component dispatches an action, the reducer produces new state from the old state and that action, selectors push the changed slice back into the component, and any effect listening to that action can do async work and dispatch a follow-up action.
One single source of truth, state is read-only and only changed by dispatching actions, and changes are made by pure reducer functions with no side effects.
When the app is small, when the state is only used by one component, or when the data is really server cache. Adding NgRx to a small app is a lot of boilerplate for no benefit. A service with a BehaviorSubject or a signal is often enough.
Both share state. NgRx adds structure and tooling: a strict update path, time-travel devtools, effects for async flows, memoised selectors and easy testing. A service is simpler but every team member can change state in their own way.
A plain object with a unique type string and optional data, describing something that happened. Actions should describe events, not commands - "Login Page: Login Submitted" rather than "Set User".
export const loadUsers = createAction("[Users Page] Load Users");
export const loadUsersSuccess = createAction(
"[Users API] Load Users Success",
props<{ users: User[] }>()
);
Because the source tells you where the action came from when you look at the devtools log, so debugging a flow is much faster. It also stops people reusing one action from many places, which makes reducers unpredictable.
A pure function that takes the current state and an action and returns the next state. It must not mutate the state, must not call APIs and must not use random values or the current time - the same input must always give the same output.
export const usersReducer = createReducer(
initialState,
on(loadUsers, (state) => ({ ...state, loading: true })),
on(loadUsersSuccess, (state, { users }) => ({ ...state, users, loading: false })),
on(loadUsersFailure, (state, { error }) => ({ ...state, error, loading: false }))
);
Purity makes it easy to test and lets devtools replay actions. Immutability lets Angular OnPush and NgRx selectors detect a change by comparing references, which is fast. Mutating state means the UI silently does not update.
Yes. Every reducer sees every action, so a single "Logout" action can reset several feature slices at once. That is a proper use of a shared action.
A function that wraps the root reducer and sees every action before or after it. It is used for logging, for resetting the whole state on logout, and for saving and restoring state from localStorage.
Not every click needs an action. Only dispatch when shared state actually changes or when a side effect must start. Local UI state, like whether an accordion is open, should stay in the component.
A function that reads a piece of state. createSelector builds a memoised selector, so it only recalculates when its inputs change and returns the same reference otherwise - which keeps OnPush components from re-rendering.
export const selectUsersState = createFeatureSelector<UsersState>("users");
export const selectAllUsers = createSelector(
selectUsersState,
(state) => state.users
);
export const selectActiveUsers = createSelector(
selectAllUsers,
(users) => users.filter((u) => u.active)
);
The selector remembers its last inputs and last result. If the inputs are the same references, it returns the cached result without running the function again. That is why derived data should be computed in selectors, not in the component.
Because it duplicates data that can go stale and makes the state bigger. Store the minimum, derive the rest in selectors.
It returns a selector for one whole feature slice by its registered key, so feature selectors do not need to know where they sit in the global state tree.
Return a function from the selector using props, or - the cleaner modern way - use a factory function that creates a selector per id. Be careful, because a factory selector loses memoisation if you create it inside the template.
export const selectUserById = (id: string) =>
createSelector(selectAllUsers, (users) => users.find((u) => u.id === id));
An effect listens to a stream of actions, does something with a side effect - usually an HTTP call - and then dispatches a new action with the result. It keeps reducers pure by moving all async work out of them.
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUsers),
switchMap(() =>
this.api.getUsers().pipe(
map((users) => loadUsersSuccess({ users })),
catchError((error) => of(loadUsersFailure({ error: error.message })))
)
)
)
);
Because an error on the outer actions stream kills the effect for good - it will never react to that action again. Catching inside the inner Observable means only that one request fails and the effect keeps listening.
switchMap for reads where only the latest matters, like a search. concatMap for writes where order matters. exhaustMap for a login or save, so double clicks do not fire twice. mergeMap when the calls are independent and can run in parallel.
An effect that only does something and does not produce a new action - navigating with the router, showing a toast, writing to localStorage. Without that option, NgRx would try to dispatch undefined and throw.
redirect$ = createEffect(
() => this.actions$.pipe(ofType(loginSuccess), tap(() => this.router.navigate(["/dashboard"]))),
{ dispatch: false }
);
With concatLatestFrom or withLatestFrom on a selector. concatLatestFrom is preferred because it reads the state lazily, only when the action actually arrives.
Provide a mock actions stream with provideMockActions, stub the API service, send the action in, and assert on the action that comes out. Because the effect is just an Observable in, Observable out, it is very testable.
An effect listens to action A and dispatches action A again, directly or through another effect. The devtools log shows the same action repeating. Fix it by making the output action distinct from the input.
A helper for storing collections. It keeps items in a normalised shape - an ids array plus an entities map - and gives ready-made addOne, upsertMany, updateOne and removeOne functions plus selectors, so you do not write array logic by hand.
const adapter = createEntityAdapter<User>();
export const initialState = adapter.getInitialState({ loading: false });
on(loadUsersSuccess, (state, { users }) => adapter.setAll(users, { ...state, loading: false }));
Because looking up or updating one item is O(1) through the entities map, instead of scanning an array, and the same item is not duplicated in several places, so it cannot get out of sync.
A newer API that groups the reducer and the state slice name together and generates the feature selector plus one selector per state property automatically. It removes a lot of repetitive selector code.
A small local store for one component or one feature. It has state, updaters, selectors and effects, but it lives and dies with the component and does not go into the global store. It is a good middle ground when global state is too heavy.
A newer store built on Angular signals instead of Observables. State is read synchronously through signals, computed values replace selectors, and it needs far less boilerplate than the classic store while keeping the same one-way flow.
A browser extension that shows every action, the state before and after, and lets you jump back to any point in time. It turns "the screen is wrong" into "this action produced the wrong state", which is much faster to debug.
Keep loading and error flags in the feature slice, set loading true on the request action, false on both success and failure, and store the error message on failure. Then expose them as selectors so the template can show a spinner or a message.
Use createActionGroup and createFeature, use Entity for collections, keep local UI state out of the store, and consider ComponentStore or the Signal Store for smaller features. Do not put everything in the global store just because it exists.
The pattern is identical. NgRx is built around RxJS and Angular dependency injection, uses effects for async work and createSelector for memoisation. Redux Toolkit uses thunks or listeners and Immer for updates. Concepts transfer directly between the two.
CrackInterviewAI listens to the live interview and gives a structured answer on screen for coding, system design, HR and project questions. Download for Windows.