Skip to content

Installing Ngxtension

Alright, let’s calm things down a bit now. We are actually going to use the connect function in this lesson, which is considerably easier than understanding how to build the thing.

The connect function is just one of the utilities found in the ngxtension library. Although it might seem wasteful to install an entire library for just one function, the library is structured to use secondary entry points such that when your Angular application is built tree-shaking will take care of removing code for all the rest of the functions that you aren’t using.

In this lesson, we will just be talking about the general usage of the connect function and in the following lessons we will refactor some of our existing code with it.

If you want, you can install this in the Quicklists application, or any other demo application, and have a play around with it:

Terminal window
ng add ngxtension

First, we are going to get the basic idea of how this works, and how it simplifies things for us. If you read through the entirety of the last lesson, then you might already have a decent idea of how this can be used.

Then in the coming lessons we will refactor the applications we have already built with this new approach — don’t worry, it’s actually quite easy because this utility is very much in line with the philosophy we have been using.

For our last application build, we will use this approach from the beginning. If you’re feeling comfortable with the general ideas we have been using for state management, then I would advise that you also just generally use this approach from now on as it removes a whole lot of boilerplate for us. If you don’t like it, feel free to continue using the approach we have been using.

Using the connect function

Let’s suppose we have a state signal that looks like this:

export type LoginStatus = 'pending' | 'authenticating' | 'success' | 'error';
interface LoginState {
status: LoginStatus;
}
// state
private state = signal<LoginState>({
status: 'pending',
});

Let’s also suppose we have some sources that looks like this:

// sources
error$ = new Subject<any>();
login$ = new Subject<Credentials>();
userAuthenticated$ = this.login$.pipe(
// ...snip
);

With our normal approach we might set up some reducers like this:

constructor() {
// reducers
this.userAuthenticated$
.pipe(takeUntilDestroyed())
.subscribe(() =>
this.state.update((state) => ({ ...state, status: 'success' }))
);
this.login$
.pipe(takeUntilDestroyed())
.subscribe(() =>
this.state.update((state) => ({ ...state, status: 'authenticating' }))
);
this.error$
.pipe(takeUntilDestroyed())
.subscribe(() =>
this.state.update((state) => ({ ...state, status: 'error' }))
);
}

You’ve seen this plenty of times by now. In this case, we are updating the status state based on which source is emitting.

Let’s see what this looks like with connect:

import { connect } from 'ngxtension/connect';
constructor() {
// reducers
const nextState$ = merge(
this.userAuthenticated$.pipe(map(() => ({ status: 'success' as const }))),
this.login$.pipe(map(() => ({ status: 'authenticating' as const }))),
this.error$.pipe(map(() => ({ status: 'error' as const })))
);
connect(this.state).with(nextState$);
}

Pretty clean, right? What is happening here is that the connect function allows us to connect any signal with any observable. Whatever that observable emits will be set into the signal. The connect handles subscribing and unsubscribing for us.

We have pretty much left our reducers exactly the same as they were, except rather than calling this.state.update and returning the next state, we map the observable to whatever we want the next state to be.

We could just do this:

constructor() {
// reducers
connect(this.state).with(
this.userAuthenticated$.pipe(map(() => ({ status: 'success' as const })))
);
connect(this.state).with(
this.login$.pipe(map(() => ({ status: 'authenticating' as const })))
);
connect(this.state).with(
this.error$.pipe(map(() => ({ status: 'error' as const })))
);
}

In this example, we just connect each observable stream with this.state separately — this will function the same. Perhaps you like this syntax better — you can use this style if you want.

But, personally, I prefer to just have all the values emit on a single nextState$ stream:

const nextState$ = merge(
this.userAuthenticated$.pipe(map(() => ({ status: 'success' as const }))),
this.login$.pipe(map(() => ({ status: 'authenticating' as const }))),
this.error$.pipe(map(() => ({ status: 'error' as const })))
);

We use the RxJS merge operator to combine all of these streams into one stream, and then we supply that in a single connect call:

connect(this.state).with(nextState$);

Notice that we have had to add as const to our strings. This is just to deal with TypeScript. It is expecting status to be of the type:

export type LoginStatus = 'pending' | 'authenticating' | 'success' | 'error';

However, if we try to set the status to just 'success' it will not work. Even though 'success' matches one of the values in our union type, TypeScript considers the type of 'success' to just be a generic string, not literally the specific string 'success'. By adding as const we are enforcing that the value will never change from 'success' and this makes TypeScript comfortable that it satisfies the LoginStatus type.

This is a particularly nice and clean example — of course, things are not always so clean and nice. We have reducers in other circumstances that deal with more complex situations, like ones that require access to the previous state.

In the next lesson, we will be refactoring the services in the Quicklists application to use this approach and this will touch on those more complex situations.