Skip to content

Instantly share code, notes, and snippets.

@vsavkin
Last active January 13, 2017 16:45
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vsavkin/3905e217ce19ac65c8a485615ae5c347 to your computer and use it in GitHub Desktop.
Save vsavkin/3905e217ce19ac65c8a485615ae5c347 to your computer and use it in GitHub Desktop.

Redux-Style Selectors.

Store

Say we have a store defined as follows:

export class Store {
  users = {1: "victor", 2: "thomas"};
  messages = {1: "victor's message", 2: "thomas's message"};

  notifications = new Subject<any>();

  constructor() {
    setTimeout(() => {
      this.users['1'] = 'VICTOR';
      this.notifications.next(null);
    }, 1000);

    setTimeout(() => {
      this.messages['1'] = "victor's new message";
      this.notifications.next(null);
    }, 2000);
  }
}

The only important part of the store is that it will emit a notification on change.

Selectors

Let's define two selectors like this.

export function getUser(store: Store) {
  return (id) => store.users[id];
}
type GetUser = (id: string) => string;

export function getMessage(store: Store) {
  return (id) => store.messages[id];
}
type GetMessage = (id: string) => string;

Utility

Next, let's define a utility function markForCheckWhenSelectorChanges.

function markForCheckWhenSelectorChanges(t: any):any {
  return {
    provide: t,
    useFactory: _markForCheckFactory,
    deps: [Store, ChangeDetectorRef, [new Inject(t), new SkipSelf()]]
  };
}

function _markForCheckFactory<T>(store: Store, ref: ChangeDetectorRef, t: T): T {
  const f = <any>t;

  let stored = [];
  let flush = false;

  const subscription = store.notifications.subscribe(() => {
    for (let i = 0; i < stored.length; ++i) {
      const s = stored[i];
      if (f(...s[0]) !== s[1]) {
        flush = true;
      }
    }

    if (flush) {
      // hack because of a bug
      (<any>ref).internalView.viewChildren[0].changeDetectorRef.markForCheck();
      flush = false;
      stored = [];
    }
  });

  return <any>((...args) => {
    const r = f(...args);
    stored.push([args, r]);
    return r;
  });
}

Basically, markForCheckWhenSelectorChanges wraps the selector function. It will record all the invocations of the selector function. Then when the store notifies about a change, the wrapped function will check if anything has changed. If it is the case, it'll request a change detection check.

Registering Selectors

Let's register the selectors in a module

@NgModule({
  providers: [
    {
      provide: getUser,
      useFactory: getUser,
      deps: [Store]
    },

    {
      provide: getMessage,
      useFactory: getMessage,
      deps: [Store]
    },

    // composing two selectors
    {
      provide: 'userAndMessage',
      useFactory: (u, m) => (userId, messageId) => ({user: u(userId), message: m(messageId)}),
      deps: [getUser, getMessage]
    }
  ],
  ...
})
class MyNgModule {}

Using Selectors

And finally, let's use them in our component like this:

@Component({
  templateUrl: `
  <h2>
    User: {{ getUser('1') }}
  </h2>

  <h2>
    Message: {{ getMessage('1') }}
  </h2>

  <h2>
    Global User and Message: {{ globalGetUserAndMessage('1', '1') | json }}
  </h2>

  <h2>
    Local User and Message: {{ localGetUserAndMessage('1', '1') | json }}
  </h2>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    markForCheckWhenSelectorChanges(getUser),
    markForCheckWhenSelectorChanges(getMessage),
    markForCheckWhenSelectorChanges('userAndMessage')
  ]
})
export class MyComponent {
  localGetUserAndMessage: Function;

  constructor(@Inject(getUser) private getUser: GetUser,
              @Inject(getMessage) private getMessage: GetMessage,
              @Inject('userAndMessage') private globalGetUserAndMessage: Function
              ) {

    // composing selectors locally          
    this.localGetUserAndMessage = (userId, messageId) => ({
      user: getUser(userId),
      messageId: getMessage(messageId)
    });
  }
}

Notes

  • markForCheckWhenSelectorChanges is required only because the component uses the OnPush change detection.
  • In this example the selectors registered separately from where they are used, but they can be registered in the same place.
  • Adding memoization is trivial.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment