Quand on est développeur on passe nos journées à écrire des fonctions. Si un enchaînement d'appel se trouve être trop long et/ou répété plusieurs fois on créera systématiquement une fonction pour rendre le code plus lisible. Quand je vois du code RxJS souvent ce n'est pas le cas, je vois souvent des gens copier-coller des enchaînements d'opérateurs avec toujours les mêmes paramètres comme si on ne pouvait pas faire autrement. Alors que créer un opérateur RxJS c'est à peine plus compliqué que créer une fonction !

Le code de base


const messages$ = from([
    {id: 0, author: { id: 10, firstname: 'David', lastname: 'Tenant' }, content: 'Allons-y, Alonzo!', timestamp: '2020-06-11 13:32:24+0200'}
    ...
]);

const authors$ = messages$.pipe(
    map(msg => msg.author),
    distinct(),
);

const firstnames$ = authors$.pipe(
    map(author => author.firstname),
    distinct()
);

const lastnames$ = authors$.pipe(
    map(author => author.lastname),
    distinct()
);

const ircStyleMessages$ = messages$.pipe(
    map(msg => `${formatDate(msg.timestamp)} ${msg.author.firstname} ${msg.author.lastname}: ${msg.content}`),
);

Qu'est ce qu'un opérateur RxJS ?

Un opérateur est une fonction qui va appliquer un certain comportement à un observable source et renvoyer un observable.

Si on prend un exemple :

const end$ = elements$.pipe(
    operator1(),
    operator2(),
);

En se basant sur la définition, on peut déduire de l'exemple que operator1 va prendre en entrée elements$, faire un traitement sur chaque élément émis par elements$, et renvoyer un observable – appelons le elements2$ – qui émettra en tant qu'élément le résultat du traitement appliqué à chaque élément de elements$. Ensuite operator2 recevra element2$ et fera la même chose.

Mais vous devez vous demander comment l'observable elements$ est passé à operator1 vu qu'on ne lui donne aucun paramètre ?

En fait il faut comprendre que lorsqu'on définit une séquence d'opérateur dans un pipe on ne fait que passer des paramètres à pipe(), et c'est pipe() qui fera le passage des observables en paramètre.

On pourrait écrire operator1 comme ça :

// with functions
function operator1() {
	return function (source$) {
    	return source$.pipe(
        	map(element => doSomethingOnElement(element))
        );
    }
}

// with arrow functions
const operator1 = () => source$ => source$.pipe(
	map(element => doSomethingOnElement(element))
);

// with a mix function/arrow function
function operator1() {
	return source$ => source$.pipe(
		map(element => doSomethingOnElement(element))
	);
}

Peu importe la syntaxe que vous préférez, vous voyez qu'on peut écrire très facilement nos opérateurs : un opérateur c'est simplement une fonction qui renvoie une fonction qui prend en paramètre un observable source et applique des traitements.

Ré-écrivons notre exemple

Sans plus attendre, voici l'exemple que je donnais une fois réécrit avec des opérateurs personnalisés.

function <T> mapDistinctKey(key) {
	return source$.pipe(
    	map(element => element[key]),
        distinct()
    );
}

function formatMessageWithIrcStyle() {
	return source$ => source$.pipe(
    	map(msg => `${formatDate(msg.timestamp)} ${msg.author.firstname} ${msg.author.lastname}: ${msg.content}`),
    );
}


const authors$ = messages$.pipe(mapDistinctKey('author'));
const firstnames$ = authors$.pipe(mapDistinctKey('firstname'));
const lastnames$ = authors$.pipe(mapDistinctKey('lastname'));

const ircStyleMessages$ = messages$.pipe(
    formatMessageWithIrcStyle(),
);

Ré-écrivons notre exemple (version Typescript)

RxJS étant écrit en TypeScript, le typage des Observable est très facile et permet de s'appuyer fortement sur l'inférence de type de TypeScript. On peut donc créer des opérateurs typés très facilement avec peu de type explicite.

function <T> mapDistinctKey(key: keyof T): Observable<T> {
	return source$.pipe(
    	map(element => element[key]),
        distinct()
    );
}

function formatMessageWithIrcStyle() {
	return (source$: Observable<Message>) => source$.pipe(
    	map(msg => `${formatDate(msg.timestamp)} ${msg.author.firstname} ${msg.author.lastname}: ${msg.content}`),
    );
}


const authors$ = messages$.pipe(mapDistinctKey('author'));
const firstnames$ = authors$.pipe(mapDistinctKey('firstname'));
const lastnames$ = authors$.pipe(mapDistinctKey('lastname'));

const ircStyleMessages$ = messages$.pipe(
    formatMessageWithIrcStyle(),
);

Avec le typage qu'on a ajouté, on aurait par exemple une erreur si on essayait d'utiliser l'opérateur formatMessageWithIrcStyle()sur un Observable émettant des string.

Conclusion

Le but n'est pas de créer des opérateurs tout le temps sans réfléchir, mais juste n'oubliez pas cette possibilité et n'hésitez pas à créer des opérateurs quand c'est plus pratique et/ou plus propre ! Rappelez-vous : un opérateur c'est une fonction, donc créez des opérateurs comme vous créez des fonctions !

Crédit photo : https://pixabay.com/photos/waterfall-mountain-forest-nature-1373183/