Angular Challenges — Round Two
This challenge series is built around practical, real-world scenarios so you can strengthen your Angular skills through hands-on work. Each exercise ends with a pull request submission, which either I or another developer will review — mirroring the workflow you'd encounter on a real project or when contributing to Open Source Software.
The starting point for this second challenge comes from a common pattern. Inside NgRx Store, you'll encounter three pillars: Effects, Reducers, and Selectors. In practice, I frequently notice developers mixing up these responsibilities. What stands out most, though, is that Selectors — a fundamental piece — are frequently misunderstood and not leveraged enough.
In this exercise, you're given an app that already works and stores data through the NgRx global store. Your assignment is to refactor it so all the data transformations happen in the appropriate location, using the correct NgRx piece.
If you haven't attempted the challenge yet, give it a shot first by heading over to Angular Challenges, then return here to see how your approach stacks up against mine. (PRs are welcome — happy to review them)
Your goal here: show the complete set of activities with details like the activity name, the lead teacher, and — when the user has admin privileges — every teacher who runs the same activity.
First, we need to pull data from the backend: the current user and all activities, where the activity structure looks like this:
export const activityType = [ 'Sport', 'Sciences', 'History', 'Maths', 'Physics',] as const;
export type ActivityType = typeof activityType[number];
export interface Person {
id: number;
name: string;
}
export interface Activity {
id: number;
name: string;
type: ActivityType;
teacher: Person;
}
For handling side effects, NgRx provides a mechanism known as an Effect. Its purpose is to keep backend communication separate from the rest of the application. Before an Effect can run, you have to dispatch an Action, which represents a unique occurrence.
NgRx Hygiene: UNIQUE matters here. Even if two situations appear to need the same side effect or state update, you must not reuse an action.
Let's set up a pair of actions: the first pulls user information, and the second retrieves the activity list.
export const loadActivities = createAction('[AppComponent] Load Activities');
export const loadUsers = createAction('[User] Load User');
An action's naming convention carries meaning: a descriptive prefix inside square brackets indicates where the action originates and is followed by a short explanation of its purpose. This pattern pays off when you're tracking down issues in the Redux DevTools.
With our actions in place, we can trigger them from the component's ngOnInit hook.
ngOnInit(): void {
this.store.dispatch(loadActivities());
this.store.dispatch(loadUsers());
}
NgRx Hygiene: avoid dispatching multiple actions. One action is capable of triggering several Effects or Reducers at once.
Given that guideline, that initial dispatch block can be simplified right away:
// single action to dispatch multiple effect to fetch all necessary data
export const initApp = createAction('[AppComponent] initialize Application');
// ngOnInit hook inside our AppComponent
ngOnInit(): void {
this.store.dispatch(initApp());
}
Next, the data-fetching Effect comes together like this:
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() => {
return this.actions$.pipe(
// we listen to only initApp action
ofType(AppActions.initApp),
concatMap(() =>
this.userService.fetchUser().pipe(
map((user) => UserActions.loadUsersSuccess({ user })),
catchError((error) => of(UserActions.loadUsersFailure({ error })))
)
)
);
});
constructor(private actions$: Actions, private userService: UserService) {}
}
@Injectable()
export class ActivityEffects {
loadActivities$ = createEffect(() => {
return this.actions$.pipe(
ofType(AppActions.initApp), // listen to the same event as UserEffect
concatMap(() =>
this.ActivityService.fetchActivities().pipe(
map((activities) =>
ActivityActions.loadActivitiesSuccess({ activities })
),
catchError(() =>
of(ActivityActions.loadActivitiesFailure())
)
)
)
);
});
constructor(
private actions$: Actions,
private ActivityService: ActivityService
) {}
}
When the HTTP request goes through, a success action gets dispatched and that's what triggers the store update. Updating state is the job of a Reducer.
Reducers are a collection of pure functions. They take the current state along with an action payload and produce a brand-new state object.
An Effect is required to emit an action on every possible outcome. In this case, that means either a success or a failure action. (Handling the error branch is non-negotiable!)
export const loadActivitiesSuccess = createAction(
'[Activity Effect] Load Activities Success',
props<{ activities: Activity[] }>() // payload of our success action
);
export const loadActivitiesFailure = createAction(
'[Activity Effect] Load Activities Failure'
);
Here's how the corresponding Reducer looks:
// key of activityState inside Store object
export const activityFeatureKey = 'activity';
export interface ActivityState {
activities: Activity[];
}
// createReducer is a big switch case
export const activityReducer = createReducer(
initialState,
// case 1: success
on(ActivityActions.loadActivitiesSuccess, (state, { activities }) => ({
...state,
activities,
})),
// case 2: failure
on(ActivityActions.loadActivitiesFailure, (state) => ({
state,
activities: [],
}))
);
Note: the "on" function is capable of listening for several actions at once.
The Reducer shown above only touches the Activity slice in the global NgRx Store. The store can be carved into multiple slices. In this setup, we're dealing with both an ActivityState and a UserState. (The User slice relies on a comparable Reducer.)
The store itself is just one large JavaScript object, and every Reducer is responsible for one key of that object.
const store = {
activity: ActivityState,
user: UserState,
// ...
}
The last leg of the journey is getting that saved data back into the component. That's exactly what Selectors handle.
Selectors are pure functions that pick out specific chunks of your state. Think of them as SQL queries against your store, extracting exactly what the template needs to render.
// select the state under activity key
export const selectActivityState =
createFeatureSelector<ActivityState>(activityFeatureKey);
// select the property "activities" defined in Activity State.
export const selectActivities = createSelector(
selectActivityState,
(state) => state.activities
);
The desired data is now just a choice of the right Selector away.
// in our AppComponent
activities$ = this.store.select(selectActivities);
<!-- template of our AppComponent -->
<h1>Activity Board</h1>
<section>
<div class="card" *ngFor="let activity of activities$ | async">
<h2>Activity Name: {{ activity.name }}</h2>
<p>Main teacher: {{ activity.teacher.name }}</p>
</div>
</section>
NgRx is deeply intertwined with RxJs Observables, which is what makes managing the asynchronous pieces of the app so smooth.
In this scenario, the view updates once the HTTP request resolves and the store changes. Should new activities appear or existing ones change in the future, the view reflects those changes automatically.
So far, everything follows a pretty standard path. The real challenge begins when we talk about putting together the list of all available teachers, which is what we'll call Status.
Putting that list together requires having both activities and user data. Since this exercise draws from a real-world scenario, the author decided to lean on an Effect to handle this particular task. The reasoning (whether that's the right call, we'll examine shortly) broke down like this:
- It represents a side effect. The goal was to process data asynchronously the moment the User and Activities are both available or change.
- The end result had to live in the store.
// Status contains all teachers doing the same activity
export interface Status {
name: ActivityType;
teachers: Person[];
}
@Injectable()
export class StatusEffects {
loadStatuses$ = createEffect(() => {
return this.actions$.pipe(
ofType(AppActions.initApp), // we can listen to the action dispatched at startup
concatMap(() =>
// we cannot use WithLatestFrom to retreive our state since
// we need to listen to user and activities changes to update our status
combineLatest([
this.store.select(selectUser),
this.store.select(selectActivities),
]).pipe(
map(([user, activities]): Status[] => {
if (user?.isAdmin) {
// loop over activities to group all teachers by type of activity
return activities.reduce(
(status: Status[], activity): Status[] => {
const index = status.findIndex(
(s) => s.name === activity.type
);
if (index === -1) {
return [
...status,
{ name: activity.type, teachers: [activity.teacher] },
];
} else {
status[index].teachers.push(activity.teacher);
return status;
}
},
[]
);
}
return [];
}),
// when is done, we return a new action to update our store
map((statuses) => StatusActions.loadStatusesSuccess({ statuses }))
)
)
);
});
constructor(private actions$: Actions, private store: Store) {}
}
In the Reducer, we listen for the success action so the Status state can be kept current:
export interface StatusState {
// list of status calculated inside the effect
statuses: Status[];
// map the type of one activity type to a given list of teachers
teachersMap: Map<ActivityType, Person[]>;
}
export const statusReducer = createReducer(
initialState,
on(StatusActions.loadStatusesSuccess, (state, { statuses }): StatusState => {
const map = new Map();
statuses.forEach((s) => map.set(s.name, s.teachers));
return {
...state,
statuses,
teachersMap: map,
};
})
);
The Reducer introduces a second field called teacherMap, which exists to make it straightforward for the Selector to retrieve the teacher list, as you'll see in a moment:
export const selectStatusState =
createFeatureSelector<StatusState>(statusFeatureKey);
export const selectStatuses = createSelector(
selectStatusState,
(state) => state.statuses
);
export const selectAllTeachersByActivityType = (name: ActivityType) =>
createSelector(
selectStatusState,
(state) => state.teachersMap.get(name) ?? []
);
And finally, this is what the component looks like:
<h1>Activity Board</h1>
<section>
<!-- loop over activity list-->
<div class="card" *ngFor="let activity of activities$ | async">
<h2>Activity Name: {{ activity.name }}</h2>
<p>Main teacher: {{ activity.teacher.name }}</p>
<span>All teachers available for : {{ activity.type }} are</span>
<ul>
<!-- for each type of activity, we get the list of teachers from our selector-->
<li
*ngFor="
let teacher of getAllTeachersForActivityType$(activity.type)
| async
"
>
{{ teacher.name }}
</li>
</ul>
</div>
</section>
// inside AppComponent
getAllTeachersForActivityType$ = (type: ActivityType) =>
this.store.select(selectAllTeachersByActivityType(type));
For each activity displayed, a function calls into the store to build the teacher list for that activity.
It works, but there's a long list of problems hiding under the surface !!!
What's Wrong:
- Derived state has no business living in the store. That leads to potential bugs since every change to the source data forces you to track down every place it's mirrored. You want one authoritative source for your data — every other derivation belongs inside a Selector.
- Components shouldn't be post-processing the output of a selector (like with the
mapoperator), nor should the view be calling a selector through a function. Any shape the template requires should be set up inside a Selector from the start. - Functions invoked from a template cost you performance. With every Change Detection cycle Angular runs, the template refreshes and all those function calls execute again. Switching components to OnPush helps, but even then, those functions will still re-run whenever the
activities$observable emits. - Spotting a combineLatest inside an Effect is a major warning sign.
Developers new to NgRx often assume every piece of data the template displays has to be stored. Fight that instinct — each fact should exist in exactly one place.
Unlocking the full potential of Selectors
A detail that often goes unnoticed is that selectors can be chained. Inside one selector, you can subscribe to any number of other selectors and merge their outputs to shape precisely what you need.
For those familiar with RxJs, selectors are essentially a combineLatest operator with an added memoization layer.
With that in mind, the earlier scenario can be reworked entirely as a Selector. There is no need for Effects or Reducers—just a clean, composable Selector.
// we combine two selectors
const selectStatuses = createSelector(
// be as precise as we can be. Don't listen to the whole user object but
// only to the necessary properties. This way, the selector will be ONLY
// rerun if the user's admin property has changed.
UserSelectors.isUserAdmin,
ActivitySelectors.selectActivities,
(isAdmin, activities) => {
if (!isAdmin) return [];
// same code as in previous effect
return activities.reduce((status: Status[], activity): Status[] => {
const index = status.findIndex((s) => s.name === activity.type);
if (index === -1) {
return [
...status,
{ name: activity.type, teachers: [activity.teacher] },
];
} else {
status[index].teachers.push(activity.teacher);
return status;
}
}, []);
}
);
This approach feels far more intuitive. Both StatusEffect and StatusReducer are now obsolete, along with the need to persist Status or teacherMap in the store. The bonus of memoization is that repeated calls to this selector don't trigger any recalculation—the cached result is simply returned. The computation only runs again when the admin property on the user changes or when the activities themselves are updated.
For the template, we can now assemble our activity object.
selectActivities = createSelector(
ActivitySelectors.selectActivities,
StatusSelectors.selectStatuses,
(activities, statuses) =>
activities.map(({ name, teacher, type }) => ({
name,
mainTeacher: teacher,
type,
availableTeachers:
statuses.find((s) => s.name === type)?.teachers ?? [],
}))
);
activities$ = this.store.select(this.selectActivities);
And the template itself becomes much cleaner. No more helper functions required—all the fields we need are already exposed within the activities$ stream.
<h1>Activity Board</h1>
<section>
<div class="card" *ngFor="let activity of activities$ | async">
<h2>Activity Name: {{ activity.name }}</h2>
<p>Main teacher: {{ activity.mainTeacher.name }}</p>
<span>All teachers available for : {{ activity.type }} are</span>
<ul>
<li *ngFor="let teacher of activity.availableTeachers">
{{ teacher.name }}
</li>
</ul>
</div>
</section>
Note: To squeeze out a bit more performance, adding a trackBy function to the ngFor directive would be a sensible tweak.
And there it is—by using the appropriate NgRx concept, the application has been transformed into something simpler, more readable, and far easier to maintain.
Wrapping up
This walkthrough has covered each of the central NgRx building blocks: Effects, Reducers, Selectors, and Actions.
We've highlighted how Selectors are frequently overlooked or misunderstood, which often leads developers to over-store data by default.
The key lesson here: store each piece of information just ONCE. Whenever you find yourself needing derived data in your store, your first thought should be Selectors.
Thank you for taking on this NgRx challenge—I hope it was both enjoyable and informative.
Additional challenges are waiting for you at Angular Challenges. Feel free to give them a go; I'd be glad to offer feedback!
Stay updated on upcoming challenges by following me on Medium, Twitter, or Github.

