Skip to content

connect

Created by Enea Jahollari Josh Morony Chau Tran

connect is a utility function that connects a signal to an observable and returns a subscription. The subscription is automatically unsubscribed when the component is destroyed. If it’s not called in an injection context, it must be called with an injector or DestroyRef.

import { connect } from 'ngxtension/connect';

Usage

It can be helpful when you want to have a writable signal, but you want to set its value based on an observable.

Connect with observables

For example, you might want to have a signal that represents the current page number, but you want to set its value based on an observable that represents the current page number from a data service.

@Component()
export class AppComponent implements OnDestroy {
private dataService = inject(DataService);
pageNumber = signal(1);
constructor() {
connect(this.pageNumber, this.dataService.pageNumber$);
}
}

Connect with observables not in an injection context

You can also use it not in an injection context, but you must provide an injector or DestroyRef.

@Component()
export class AppComponent implements OnDestroy {
private dataService = inject(DataService);
private injector = inject(Injector);
// or
private destroyRef = inject(DestroyRef);
pageNumber = signal(1);
ngOnInit() {
connect(this.pageNumber, this.dataService.pageNumber$, this.injector);
// or
connect(this.pageNumber, this.dataService.pageNumber$, this.destroyRef);
}
}

Connect with other signals

This is useful when you want to have a writable signal, but you want to update its value based on another signal that represents the current state of that value. For this to work, the second argument must be a callback function that includes a signal call inside of it.

@Component()
export class AppComponent implements OnDestroy {
private dataService = inject(DataService);
pageNumber = signal(1);
constructor() {
connect(this.pageNumber, () => this.dataService.state().pageNumber);
}
}

Object Signal

There are cases where we construct a single Signal to store a state object. connect can also work with object signals

@Component()
export class MyComponent {
state = signal({
user: {
firstName: 'chau',
lastName: 'tran',
},
});
firstName = computed(() => this.state().user.firstName);
lastName = computed(() => this.state().user.lastName);
lastName$ = new Subject<string>();
constructor() {
effect(() => {
console.log('first name changed', this.firstName());
});
// we want to connect `lastName$` stream to `state`
// and when `lastName$` emits, update the state with the `reducer` fn
connect(this.state, this.lastName$, (prev, lastName) => ({
user: { ...prev.user, lastName },
}));
// logs: first name changed, chau
// sometimes later
this.lastName$.next('Tran');
// `firstName()` effect won't be triggered because we only update `lastName`
}
}

ConnectedSignal

A ConnectedSignal allows you to connect any number of streams to a signal during or after the initial connect call.

const connectedSignal = connect(this.state)
.with(this.lastName$, (prev, lastName) => ({ user: { ...prev.user, lastName } }))
.with(this.firstName$, (prev, firstName) => ({ user: { ...prev.user, firstName } }));
/* can connect later as well */
connectedSignal.with(/* ... */);
/* can destroy */
connectedSignal.subscription.unsubscribe();
/* after the subscription is closed, connectedSignal doesn't so anything */
connectedSignal.with(/* ...*/)); // won't connect

A benefit of this approach is that it allows you to connect multiple streams to a signal whilst utilising different syntax for the connect call.

For example, if your streams directly emit the values you want to set into the signal, you can use this syntax:

connect(this.pageNumber, this.dataService.pageNumber$);

Or, if you need to use a reducer to access the previous signal value, you can use this syntax:

connect(this.state, this.lastName$, (prev, lastName) => ({
user: { ...prev.user, lastName },
}));

However, if you want to use multiple different streams with different reducers, you would need to use multiple connect calls (one for each reducer you want to add), e.g:

connect(this.state, this.someStream$);
connect(this.state, this.add$, (state, checklist) => ({
checklists: [...state.checklists, checklist],
}));
connect(this.state, this.remove$, (state, id) => ({
checklists: state.checklists.filter((checklist) => checklist.id !== id),
}));

With a ConnectedSignal you can use the with syntax to chain these into a single connect call:

connect(this.state)
.with(this.someStream$)
.with(this.lastName$, (prev, lastName) => ({
user: { ...prev.user, lastName },
}))
.with(this.firstName$, (prev, firstName) => ({
user: { ...prev.user, firstName },
}));

This allows for any combination of streams without reducers and streams with different types of reducers.