Redux-style selectors for Angular 2.
Let's define our store as follows:
export class Store {
users:any = {1: "victor", 2: "thomas"};
messages:any = {1: "victor's message", 2: "thomas's message"};
notifications = new Subject<any>();
updateUser(id: string, value: string): void {
this.users[id] = value;
this.notifications.next(null);
}
updateMessage(id: string, value: string): void {
this.messages[id] = value;
this.notifications.next(null);
}
}
Then, let's define two basic selectors.
type GetUser = (id: string) => string;
export function getUser(store: Store) {
return (id: string) => store.users[id];
}
type GetMessage = (id: string) => string;
export function getMessage(store: Store) {
return (id: string) => store.messages[id];
}
Next, let's register them in our NgModule.
@NgModule({
declarations: [TestComponent],
providers: [
Store,
{
// this tells the library when to check if selectors changed
provide: CHANGES,
useFactory: (s:Store) => s.notifications,
deps: [Store]
},
{
provide: getUser,
useFactory: getUser,
deps: [Store]
},
{
provide: getMessage,
useFactory: getMessage,
deps: [Store]
}
]
})
class TestNgModule { }
And finally, let's add a component using them.
@Component({
selector: 'test',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
// this wraps the functions defined in the ng module
selector(getUser),
selector(getMessage)
]
})
class TestComponent {
recorded: any[] = [];
constructor(@Inject(getUser) private u: GetUser,
@Inject(getMessage) private m: GetMessage){}
ngDoCheck() {
this.recorded.push({
user: this.u('1'),
message: this.m('1')
});
}
}
Let's start by defining a new selector that uses the two previously defined selectors.
@NgModule({
declarations: [TestComponent],
providers: [
//...
{
provide: 'getUserAndMessage', // compose the two values
useFactory: (u: GetUser, m: GetMessage) =>
(userId: string, messageId: string) => ({user: u(userId), message: m(messageId)}),
deps: [getUser, getMessage]
}
]
})
class TestNgModule { }
Updated component.
@Component({
selector: 'test',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
selector(getUser),
selector(getMessage),
// since the new function returns a new object every single time
// we cannot rely on the default equality check (which is a reference check)
selector('getUserAndMessage', {equality: shallowEqual})
]
})
class TestComponent {
recorded: any[] = [];
constructor(@Inject(getUser) private u: GetUser,
@Inject(getMessage) private m: GetMessage,
@Inject('getUserAndMessage') private um: any){}
ngDoCheck() {
this.recorded.push({
user: this.u('1'),
message: this.m('1'),
userAndMessage: this.um('1', '1')
});
}
}
The only interesting part of this library is the selector
helper. It takes a DI token, fetches it, and decorates it. The decorated function will record all the invocations to determine if the component needs to be rechecked.
For instance getUserAndMessage
will record all the invocations of this function. Once the CHANGES observable emits an event, the library will recheck all the invocations of getUserAndMessage
to see if any of them resulted in a different value. If this is the case, the library will request a change detection check for the test component.
So the library does the minimum amount of change detection checks required. In the example above, only when User 1 or Message 1 changes, the test component will be rechecked. If any other user or message changes, the component WON'T be rechecked.
To make it work with Angular CLI, do the following:
Add ngselectors to your package.json:
"ngselectors": "https://github.com/vsavkin/ngselectors"
Add this to angular-cli-build.js
'ngselectors/**/*.js',
Update system-config.ts
const barrels: string[] = [
// Thirdparty barrels.
'ngselectors'
];
const cliSystemCo
System.config({
map: {
'ngselectors': 'vendor/ngselectors/build'},
packages: cliSystemConfigPackages
});