Maintaining your container Component state through RxAngular

We need to pay more attention to the importance of managing the component state. It doesn’t get the attention deserved.

Ankit kaushik
3 min readFeb 18, 2023

To make your component as thin as possible I suggest abstracting your component state in a facade. The basic idea here would be to inject component-level services to hold the business entity depicted in your container component.

Yes, we’ll be using the above library this time to showcase the same around the following use case:

  • Rendering paginated (infinite scroll) list of open events
  • Rendering paginated (infinite scroll) list of closed events
  • Whenever the user closes any open event, add that to the closed events list with a few manipulated properties and at the same time remove it from the open events list.

Example undertaking for this article

demonstration of two lists namely open and closed events list which are related to each other in some way
Demonstration of two lists which are related in some way

State for open events

// state for the open events component
{
count: number;
pageNumber: number;
events: IUserEvent[];
}

Defining events that need to be fetched

Depends on the current page and the userId for which the events need to be fetched

userEvents$ = combineLatest([
this.userId$, // observable for retrieving userId
this.select('pageNumber'), // this is how we listen to a peice of state in RxState
]).pipe(
switchMap(([userId, pageNumber]) =>
this.fetchUserEvents(userId, pageNumber) // API call to fetch user events
),
map(({ data }) => data)
);

Maintaining the state of userEvents on each emitted value set

this.connect(this.userEvents$, (oldState, newEmittedValue) => {
if (oldState.pageNumber > 1) { // for pagination
return {
events: oldState.events.concat(newEmittedValue.events),
};
}
return value; // reset the state to the new emitted value (page 1 events)
});

There’s an action as well (close these events), Let’s define that

// Action to be fired from within the component
closeEvents$$ = new Subject<{
eventsToClose: IUserEvent[];
resolution: Record<'reason' | 'note', string>;
}>();

Anytime selected events are closed, that makes an update API call and marks those events as closed. Also, they need to be subtracted from the open events collection. This gives birth to a related state (eventsClosed$) to listen to and it should fire after the events are closed.

eventsClosed$ = this.closeEvents$$.pipe( // on closeEvents$$ action
exhaustMap(({ eventsToClose, resolution }) =>
this.closeEvents(eventsToClose, resolution) // Api call to persist the change
),
share()
);

Manipulate the state once the selected events are closed

this.connect(this.eventsClosed$, (oldState, value) => {
const closedEventsIdSet = value.reduce(
(acc, e) => acc.add(e.id),
new Set()
);

// updated state
return {
events: oldState.events.filter(
(oEvent) => closedEventsIdSet.has(oEvent.id) === false
),
count: oldState.count - value.length,
};
});

Similarly, you can form the state facade for the closed event list as well


interface ICloseEventsState {
count: number;
pageNumber: number;
events: IUserEvent[];
}

enum EventStatus {
open = 0,
close = 1,
}

@Injectable()
export class CloseEventsState extends RxState<ICloseEventsState> {
readonly closeEvents$ = this.select('events');
readonly totalCount$ = this.select('count');

private http = inject(HttpClient);
private store = inject(Store);
private userId$ = this.store.select(USER_STATE_TOKEN).pipe(
map(({ id }) => id),
distinctUntilChanged()
);

private userClosedEvents$ = combineLatest([
this.userId$,
this.select('pageNumber'),
]).pipe(
switchMap(([userId, pageNumber]) =>
this.fetchUserEvents(userId, pageNumber)
),
map(({ data }) => data)
);

constructor(private openEventsState: OpenEventsState) {
super();
this.set({
pageNumber: 1,
});

this.connect(this.userClosedEvents$, (oldState, value) => {
if (oldState.pageNumber > 1) {
return {
events: oldState.events.concat(value.events),
};
}
return value;
});

// Derving state based on the event fired in facade for open event list
this.connect(this.openEventsState.eventsClosed$, (oldState, value) => ({
events: value.concat(oldState.events),
count: oldState.count + value.length,
}));
}

private fetchUserEvents(userId: string, pageNumber: number) {
return this.http
.get<IResponse<IUserEventsData>>(
`vital/v1/users/${userId}/events`,
{
params: {
status: EventStatus.close,
pageNumber,
recordPerPage: 25,
},
}
);
}
}

If UserEventsComponent is your container component for both the presentation components ( OpenEventsComponent, ClosedEventsComponent). Then these states ( OpenEventsState, ClosedEventsState)have to be provided on your container component namely UserEventsComponent

@Component({
...
providers: [OpenEventsState, ClosedEventsState]
})
export class UserEventsComponent {
...
...
}

With this, I leave you with the task of figuring out numerous opportunities to abstract your container component state in these elegant facades.

--

--