- 1. Ressources
- 2. Constat
- 3. Redux : les différents concepts utilisés
- 4. Les 3 principes fondamentaux de Redux
- 5. Pourquoi NgRx ?
- 6. Démo : @ngrx/store avec une appli
todos
- 7. L’outils NgRx Store Devtools
- 8. 3 exemples regroupés ici, présentés durant la conférence
- 9. NgRx Entity
- 10. NgRx Router Store
- 11. Conclusion
Faire du Redux avec Angular et NgRx v4 - Bruno Baia (@brbaia) - GDG Toulouse
-
Vidéo : https://youtu.be/IBFXLgCw5Ek
-
Slides : http://slides.com/brunobaia/redux-with-angular-ngrx
-
Code source : https://github.com/bbaia/gdgtoulouse-ngrx
-
La bible : https://redux.js.org
-
Ensemble des frameworks utilisés : https://github.com/ngrx/platform
📎
|
Retrouvez le listing de mes mémos, tips et autres sur https://github.com/jprivet-dev/memos |
Les applications web (on parle de web apps et non plus de sites web) deviennent de plus en plus compliquées. Il est possible aujourd’hui de faire des clients riches/lourds avec du JavaScript.
Ces applications web deviennent de plus en plus complexes, tout est connecté à tout, et l’état de ces applications change beaucoup.
Ces changements viennent de différents endroits :
-
Etat côté… :
-
serveur
-
client
-
-
Les données… :
-
en cache
-
du websocket en live
-
créées en local
-
liées à l’interface graphique
-
de l’URL
-
Dès que vous touchez quelque chose dans l’application, vous cassez facilement quelque chose ailleur. Le pattern MVC a atteint ses limites pour ce type d’application.
Facebook a proposé Redux, un framework JavaScript qui permet de gérer l’état applicatif de manière plus prédictible.
Redux s’inspire et se base sur les concepts suivant.
Flux unidirectionel des données : les composants vont communiquer entre eux toujours dans le même sens (pas de data binding double).
On sépare tout ce qui est écriture de tout ce qui est lecture. Pour l’écriture on a besoin de transaction, de cohérence, d’intégrité et de normalisation alors que pour la lecteur ce n’est pas nécessaire.
On gère les changements de l’application comme une séquence d’événements. On ne se préocuppe plus de l’état en cours de l’application : on veut simplement connaître la suite des événements qui permettent d’arriver à l’état actuel de l’application.
Les objets ne sont plus modifiables après les avoir créés :
-
+ de performance
-
+ de cohérence
-
Facilite le debbuging
-
Les composants vont émettre des intentions, des actions.
-
Les actions vont modifier l’état de l’application via des reducers.
-
À chaque fois que le store évolue, que l’état de l’application est modifié, cela met à jour les composants qui se sont abonnés.
"The state of your whole application is stored in an object tree within a single store".
L’état est la seule source de vérité de l’application.
Votre état va être gérée avec un POJO (Plain Old JavaScript Object), un object basique JavaScript (avec des propriétés, des valeurs et des tableaux) pour stocker l’état de manière simple :
{
"todos": [ // (1)
{
"text": "Consider using Redux",
"completed": true
},
{
"text": "Keep all state in a single tree",
"completed": false
}
],
"filter": "SHOW_ALL" // (2)
}
-
Tableau de tâches, avec un texte et un état par tâche
-
Filtre que l’on souhaite appliquer pour l’affichage de la liste des tâches (pour afficher tous les
todos
, uniquement ceux qui sont complétés, …)
"The only way to change the state is to emit an action, an object describing what happened".
L’état est en lecture seule (principe d’immutabilité).
Il ne sera pas possible de faire :
todos.completed = false
Pour mettre à jour le store, il sera nécessaire de passer par une action. Exemples de payloads (propriétés d’une action) :
{
"type": "ADD_TODO", // (1)
"text": "Visit delair.aero"
}
-
Ici on souhaite ajouter une nouvelle tâche
{
"type": "SET_VISIBILITY_FILTER",
"filter": "SHOW_COMPLETED" // (1)
}
-
Ici on souhaite changer le filtre et afficher uniquement les
todos
complétés
"To specify how the state tree is transformed by actions, you write pure functions (reducers)"
Les reducers vont être les seuls éléments qui vont permettre de modifier l’état de l’application.
Les fonctions pures sont des fonctions qui vont toujours renvoyer la même chose si on leur donne en entrée toujours les mêmes infos (cela facilitera les tests).
Si par exemple, une fonction se base sur une variable globale, on ne pourra plus la considérer comme pure car si on l’appelle deux fois de suite avec les mêmes paramètres, elle pourrait renvoyer deux résultats différents.
Exemple de reducer
:
function todos(state = {}, action) {
switch (actions.type) { // (1)
case 'ADD_TODO':
return { // (2)
...state, // (3)
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
};
case 'SET_VISIBILITY_FILTER':
return {
...state,
visibilityFilter: action.filter // (4)
};
};
}
-
On va parcourir tous les types d’actions.
-
Ici on met à jour la propriété
todos
dustate
, en retournant un nouvel objet auquel est concaténé àstate.todos
une nouvelle tâche. -
Usage de la syntaxe de décomposition
…
(spread syntax). -
Ici on met à jour la propriété
visibilityFilter
dustate
avec la nouvelle valeuraction.filter
.
❗
|
On ne modifie jamais l’état en cours, on renvoie toujour un nouvel object pour l’état. |
Cela va être plus facile de comprendre l’application :
-
On a un flux de données undirectionnel.
-
On sait d’où vienne les données.
-
On sait où va se produire le problème.
-
Il est plus facile de mentaliser le fonctionnement.
-
Il est plus facile de savoir où placer son code.
-
Avoir des POJO, pour gérer l’état et les actions, facilite le debug et l’inspection.
-
Comme l’état est sérializable et qu’il est possible de le persister, il sera posssible de revenir dans le temps, de supprimer des actions. On va pouvoir débuger, faire du hot module reloading, etc.
-
Ng ⇒ Angular
-
Rx ⇒ RxJS
NgRx s’adapte très bien à la partie programmation fonctionnelle et réactive d’Angular, avec le async pipe, le RxJS, le onpush strategy, …
@ngrx/store
est le module commun, la base pour gérer le state
de l’application.
Structure de l’application :
app
|-- store
| `-- meta-reducers.ts (1)
|-- todos
| |-- component
| | |-- footer (2)
| | | |-- footer.component.html
| | | `-- footer.component.ts
| | |-- new-todo (3)
| | | |-- new-todo.component.html
| | | `-- new-todo.component.ts
| | |-- todo (4)
| | | |-- todo.component.html
| | | `-- todo.component.ts
| | |-- todo-list
| | `-- todo-list-item
| |-- models
| | |-- index.ts
| | |-- todo-filter.model.ts
| | `-- todo.model.ts
| `-- store
| |-- actions.ts (5)
| |-- effects.ts (6)
| |-- index.ts
| |-- reducers.ts (7)
| `-- selectors.ts (8)
|-- assets
`-- environments
-
meta-reducers
: lestoreFreeze
est configurés ici. -
footer
: c’est un composant basique "dumb" (il ne va faire que de la présentation). Il affiche le nombre detodos
restant, les filtres… -
new-todo
: composant pour créer destodos
. -
todo
: c’est un composant de typecontainer
, c’est à dire qu’il va être pure. Il ne fait que sélectionner des états et dispatcher des actions. -
actions
: les composants vont émettre des actions. Les actions vont modifier l’état de l’application via des reducers. -
effects
: les effets vont nous permettre d’écouter les actions. -
reducers
: ce sont les seuls éléments qui vont permettre de modifier l’état de l’application. -
selectors
: les sélecteurs vont nous permettre de récupérer des données calculées à chaud, qui ne seront pas stocker dans lestore
.
Exemple de fichier :
// footer.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { TodoFilter } from '../../models';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
})
export class FooterComponent {
@Input() undoneTodosCount: number; // (1)
@Input() currentFilter: TodoFilter; // (2)
@Output() filter = new EventEmitter<TodoFilter>(); // (3)
@Output() clearCompleted = new EventEmitter(); // (4)
}
-
Nombre de
todos
restants -
Le filtre en cours utilisé
-
Événement lorsque l’on va vouloir filtrer
-
Événement lorsque l’on va vouloir supprimer tous les
todos
en complétés
Actions are one of the main building blocks in NgRx. Actions express unique events that happen throughout your application. From user interaction with the page, external interaction through network requests, and direct interaction with device APIs, these and more events are described with actions.
https://ngrx.io/guide/store/actions
Dans actions.ts
on va retrouver les actions suivantes :
// actions.ts
export const ADD_TODO = '[TODO] add';
export const DELETE_TODO = '[TODO] delete';
export const TOGGLE_TODO = '[TODO] toggle';
export const UPDATE_TODO = '[TODO] update';
export const LOAD_TODOS = '[TODO] load';
export const LOAD_TODOS_COMPLETED = '[TODO] load completed';
export const CLEAR_COMPLETED_TODO = '[TODO] clear completed';
export const SET_TODO_FILTER = '[TODO] Set filter';
// ...
export class SetFilterAction implements Action {
readonly type = SET_TODO_FILTER;
constructor(public filter: TodoFilter) {}
}
Avec NgRx on peut créer des actions à partir de classe, ce qui est pratique au niveau du typage. Ici il sera plutôt utilisé des objets POJO.
Grace à l’outil NgRx Store Freeze, on ne peut pas rajouter de propriété à un objet qui n’est pas extensible. On ne peut pas modifier les états ou les actions (ils sont censés être immutables).
Plus tard ce sera le composant "container" todo.component.ts qui dispatchera les actions :
// todo.component.ts
// ...
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
})
export class TodoComponent implements OnInit {
// ...
constructor(private store: Store<fromTodos.State>) {
// ...
}
ngOnInit() {
this.store.dispatch(new fromTodos.LoadAction());
}
onAddTodo(text: string) {
this.store.dispatch(new fromTodos.AddAction(text));
}
onToggle(id: number) {
this.store.dispatch(new fromTodos.ToggleAction(id));
}
onUpdate(event: { id: number; text: string }) {
this.store.dispatch(new fromTodos.UpdateAction(event.id, event.text));
}
onDelete(id: number) {
this.store.dispatch(new fromTodos.DeleteAction(id));
}
onFilter(filter: TodoFilter) {
this.store.dispatch(new fromTodos.SetFilterAction(filter));
}
onClearCompleted() {
this.store.dispatch(new fromTodos.ClearCompletedAction());
}
}
Reducers in NgRx are responsible for handling transitions from one state to the next state in your application. Reducer functions handle these transitions by determining which actions to handle based on the action’s type.
https://ngrx.io/guide/store/reducers
📎
|
Comme expliqué précédemment, les reducers vont être les seuls éléments qui vont permettre de modifier l’état de l’application. |
// reducers.ts
// ...
export interface State { // (1)
todos: Todo[];
filter: TodoFilter;
}
const initialState: State = { // (2)
todos: [],
filter: 'SHOW_ALL',
};
export function reducer( // (3)
state: State = initialState,
action: fromTodos.TodoActionType,
): State {
switch (action.type) {
case fromTodos.ADD_TODO: {
return { /* ... */ }
}
case fromTodos.TOGGLE_TODO: {
return { /* ... */ }
}
case fromTodos.DELETE_TODO: {
return { /* ... */ }
}
case fromTodos.UPDATE_TODO: {
return { /* ... */ }
}
case fromTodos.CLEAR_COMPLETED_TODO: {
return { /* ... */ }
}
case fromTodos.SET_TODO_FILTER: {
return { /* ... */ }
}
default: {
return state;
}
}
}
// ...
-
L’état de l’application est typé
-
L’état est initialisé avec une liste de
todos
vide et avec un filtre qui affiche tout -
On retrouve ici le
reducer
Ci-après les modèles utilisés :
// todo-filter.model.ts
export type TodoFilter = 'SHOW_ALL' | 'SHOW_ACTIVE' | 'SHOW_COMPLETED';
// todo.model.ts
export interface Todo {
id: number;
text: string;
creationDate: Date;
completed: boolean;
}
Exemple de reducer
avec ADD_TODO
:
// reducers.ts
switch (action.type) {
case fromTodos.ADD_TODO: {
return { // (1)
...state,
todos: [ // (2)
...state.todos, // (3)
{
id: action.id,
text: action.text,
completed: false,
}
],
}
}
}
-
On retourne un nouvel objet avec un
todo
supplémentaire -
On met à jour la propriété
todos
dustate
-
On concatène un nouveau
todo
au tableaustate.todos
Exemple qui ne fonctionnerait pas :
// reducers.ts
switch (action.type) {
case fromTodos.ADD_TODO: {
state.todos.push({ // (1)
id: action.id,
text: action.text,
completed: false,
});
return state; // (2)
}
}
-
On rajoute un nouveau
todo
au tableaustate.todos
-
On retourne l’objet
state
mis à jour
Dans ce cas on se retrouvera avec une erreur en console TypeError: Cannot add property 0, object is not extensible.
📎
|
Grâce à l’outils NgRx Store Freeze, on ne peut pas modifier le state car il est immutable.
|
Selectors are pure functions used for obtaining slices of store state. @ngrx/store provides a few helper functions for optimizing this selection. Selectors provide many features when selecting slices of state: Portability , Memoization, Composition, Testability, Type Safety.
https://ngrx.io/guide/store/selectors
Les selectors vont permettre de limiter le nombre de données à enregistrer dans le store.
Les composants graphiques peuvent s’abonner aux états de l’application. L’idée serait de s’abonner à certaines parties (aux changements du filtre, aux modifications des todos
, …) et non à l’ensemble de l’application. C’est ce que permettent de faire les selectors.
// selectors.ts
export const getTodos = createSelector(/* ... */);
export const getLoading = createSelector(/* ... */);
export const getFilter = createSelector(/* ... */);
export const getFilteredTodos = createSelector(/* ... */);
export const getHasTodos = createSelector(/* ... */);
export const getUndoneTodosCount = createSelector(/* ... */);
Par exemple le getFilteredTodos
va retourner directement la liste des todos
filtrées sans qu’il soit nécessaire d’enregistrer cette liste dans le store :
// selectors.ts
export const getFilteredTodos = createSelector(
getAllTodos,
getFilter,
(todos, filter) => {
switch (filter) {
default:
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
}
},
);
Ou bien, au lieu d’enregistrer dans l’état un attribut qui indique s’il y a des todos
dans la liste ou non, il sera possible de récupérer dynamiquement cette information avec getHasTodos
:
// selectors.ts
export const getHasTodos = createSelector(getTotalTodos, totalTodos => {
return totalTodos > 0;
});
💡
|
Les selectors sont composables : on peut créer des sélecteurs avec plusieurs autres sélecteurs.
|
Ensuite ces sélecteurs peuvent être utilisés dans un composant. Par exemple, dans todo.component.ts
, il va être possible de s’abonner aux 4 événements qui nous intéressent :
// todo.component.ts
export class TodoComponent implements OnInit {
hasTodos$: Observable<boolean>;
undoneTodosCount$: Observable<number>;
currentFilter$: Observable<TodoFilter>;
filteredTodos$: Observable<Todo[]>;
loading$: Observable<boolean>;
constructor(private store: Store<fromTodos.State>) {
this.hasTodos$ = this.store.select(fromTodos.getHasTodos);
this.undoneTodosCount$ = this.store.select(fromTodos.getUndoneTodosCount);
this.currentFilter$ = this.store.select(fromTodos.getFilter);
this.filteredTodos$ = this.store.select(fromTodos.getFilteredTodos);
this.loading$ = this.store.select(fromTodos.getLoading);
}
// ...
}
De plus dans le template todo.component.html
, grace au | async
(pipe async), il ne sera pas nécessaire de gérer les subscribe
et les unsubscribe
:
<header class="header">
<h1>todos</h1>
<app-new-todo *ngIf="!(loading$ | async); else loading"
(addTodo)="onAddTodo($event)"></app-new-todo>
</header>
<app-todo-list [todos]="filteredTodos$ | async"
(toggle)="onToggle($event)"
(update)="onUpdate($event)"
(delete)="onDelete($event)">
</app-todo-list>
<app-footer *ngIf="hasTodos$ | async"
[undoneTodosCount]="undoneTodosCount$ | async"
[currentFilter]="currentFilter$ | async"
(filter)="onFilter($event)"
(clearCompleted)="onClearCompleted()"></app-footer>
<ng-template #loading>
<div>loading...</div>
</ng-template>
📎
|
|
Effects are an RxJS powered side effect model for Store. Effects use streams to provide new sources of actions to reduce state based on external interactions such as network requests, web socket messages and time-based events.
https://ngrx.io/guide/effects
Si l’on souhaite faire un appel réseau, faire un appel à un websocket, il ne sera pas possible de le faire dans un reducer
, qui est une fonction pure. Nous voulons aussi éviter de faire cet appel dans le composant graphique.
Les effects
vont nous permettre d’écouter les actions
et d’appliquer des effets en parallèle.
Exemple :
// effects.ts
// ...
@Injectable()
export class TodosEffects {
@Effect()
loadTodos$: Observable<Action> = this.actions$ // (1)
.ofType(fromTodos.LOAD_TODOS) // (2)
.switchMap(action =>
// Simulate network call
Observable.of(
new fromTodos.LoadCompletedAction([ // (3)
{
id: 0.123456789,
text: 'Remove before flight!',
creationDate: new Date(2018, 1, 6),
completed: false,
},
]),
).delay(1000), // (4)
);
// ...
constructor(
private actions$: Actions,
private router: Router,
private todosStore: Store<fromTodos.State>,
) {}
}
// ...
-
this.actions$
: observable qui retourne toutes les actions de l’application -
LOAD_TODOS
: on s’abonne uniquement à l’actionLOAD_TODOS
-
LoadCompletedAction
: on simule ici un appel réseau et on retourne une liste de tâches -
delay(1000)
: on renvoie le résultat dans un délai d’une seconde
Il est possible d’utiliser l’outils NgRx Store Devtools :
On peut suivre et observer l’état de l’application :
On peut observer les différences entre l’état d’avant et celui d’après :
On peut observer les actions :
Il est possible de simuler une action :
Il est possible de faire du time travel debuging, revenir dans le temps et naviguer dans les différents états :
// reducers.js
export function reducer(
state: State = initialState,
action: fromTodos.TodoActionType,
): State {
// ...
switch (action.type) {
case fromTodos.CLEAR_COMPLETED_TODO: {
return {
...state,
todos: state.todos.filter(todo => !todo.completed),
filter: state.filter === 'SHOW_COMPLETED' ? 'SHOW_ALL' : state.filter // (1)
};
}
}
// ...
}
-
Après un
CLEAR_COMPLETED_TODO
, si on est sur leSHOW_COMPLETED
alors on revient au filtreSHOW_ALL
On va simuler une réquête réseau pour récupérer la liste des todos
. On va pour cela rajouter des actions actions.ts
:
// actions.ts
export const LOAD_TODOS = '[TODO] load'; // (1)
export const LOAD_TODOS_COMPLETED = '[TODO] load completed'; // (2)
export class LoadAction implements Action {
readonly type = LOAD_TODOS; // (3)
}
export class LoadCompletedAction implements Action {
readonly type = LOAD_TODOS_COMPLETED;
constructor(public todos: Todo[]) {} // (4)
}
-
LOAD_TODOS
peut être déclenchée par un bouton ou au lancement de l’application -
LOAD_TODOS_COMPLETED
sera a déclencher après la réponse du serveur -
LoadAction
n’a pas de paramètres d’entrée -
LoadCompletedAction
va prendre la liste destodos
issue de la requête
On va mettre ensuite à jour le store avec la réponse de la requête :
// reducers.ts
// ...
export function reducer(
state: State = initialState,
action: fromTodos.TodoActionType,
): State {
switch (action.type) {
// ...
case fromTodos.LOAD_TODOS_COMPLETED: { // (1)
return {
...state,
todos: action.todos,
};
}
// ...
default: {
return state;
}
}
}
-
Mise à jour du store avec la liste des
todos
retourner par le serveur
Au niveau du composant, on va lancer le chargement à son initialisation, au chargement de l’application :
// todo.component.ts
export class TodoComponent implements OnInit {
// ...
ngOnInit() {
this.store.dispatch(new fromTodos.LoadAction());
}
// ...
}
On va ensuite définir l’effet à déclencher à partir de l’action LOAD_TODOS
:
// effects.ts
export class TodosEffects {
@Effect()
loadTodos$: Observable<Action> = this.actions$ // (1)
.ofType(fromTodos.LOAD_TODOS) // (2)
.switchMap(action =>
// Simulate network call
Observable.of( // (3)
new fromTodos.LoadCompletedAction([
{
id: 0.123456789,
text: 'Remove before flight!',
completed: false,
},
]),
).delay(1000), // (4)
);
// ...
}
-
On retrouve ici un observable qui va retourner toutes les actions de l’application.
-
On va ensuite s’abonner uniquement à l’action
LOAD_TODOS
. -
On simule ici, avec RxJs, la liste des
todos
à récupérer. -
On simule un délai de réponse d’une seconde.
📎
|
Fichiers modifiés : https://github.com/bbaia/gdgtoulouse-ngrx/compare/effects-1…effects-2 |
export interface State {
todos: todoEntity.State;
loading: boolean; // (1)
}
const initialState: State = {
todos: todoEntity.initialState,
loading: false, // (2)
}
-
Il faut rajouter à l’état de l’application un attribut loading de type booléen
-
Il faut initialiser l’état à false
// reducers.ts
// ...
export function reducer(
state: State = initialState,
action: fromTodos.TodoActionType,
): State {
switch (action.type) {
// ...
case fromTodos.LOAD_TODOS: {
return {
...state,
loading: true, // (1)
};
}
case fromTodos.LOAD_TODOS_COMPLETED: {
return {
...state,
todos: todoEntity.adapter.addAll(action.todos, state.todos),
loading: false, // (1)
};
}
// ...
}
}
-
Dans le
reducer
, il faut mettre à jour l’attributloading
de l’actionLOAD_TODOS
etLOAD_TODOS_COMPLETED
Il ne reste plus qu’à utiliser l’observable loading$
dans le template :
// todo.component.ts
export class TodoComponent implements OnInit {
loading$: Observable<boolean>;
constructor(private store: Store<fromTodos.State>) {
this.loading$ = this.store.select(fromTodos.getLoading);
}
// ...
}
<!-- todo.component.html -->
<header class="header">
<h1>todos</h1>
<!-- <1> -->
<app-new-todo
*ngIf="!(loading$ | async); else loading"
(addTodo)="onAddTodo($event)"></app-new-todo>
</header>
<app-todo-list [todos]="filteredTodos$ | async"
(toggle)="onToggle($event)"
(update)="onUpdate($event)"
(delete)="onDelete($event)">
</app-todo-list>
<app-footer *ngIf="hasTodos$ | async"
[undoneTodosCount]="undoneTodosCount$ | async"
[currentFilter]="currentFilter$ | async"
(filter)="onFilter($event)"
(clearCompleted)="onClearCompleted()"></app-footer>
<ng-template #loading>
<div>loading...</div>
</ng-template>
-
On n’a plus qu’à s’abonner à
loading$
📎
|
|
On va avoir plein d’entités à gérer dans notre application. Par exemple dans un blog avec des commentaires, des posts, des users, …
NgRx Entity va normaliser l’état de l’application et faciliter la manipulation de ces données, ainsi que la mise à jour dans le store de façon à ce que les données restent immutables.
On va éviter la duplication de données, éviter l’imbrication d’objets dans les objets et stocker les données à plat (comme dans une base de donnée). Chaque type de données aura sa propre table. Chaque data table
aura un id pour l’identifier. Par exemple pour faire un tri, on pourra n’utiliser que des ids.
Dans l’état on va ainsi retrouver les ids stockés à part et les entités dans un autre objet :
On va pouvoir utiliser des adapteurs :
// store/entities/todo.ts
// ...
function sortByCreationDate(a: Todo, b: Todo): number {
return b.creationDate.getTime() - a.creationDate.getTime(); // (2)
}
export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>({
selectId: (todo: Todo) => todo.id, // (1)
sortComparer: sortByCreationDate, // (2)
});
// ...
-
Là on peut récupérer notre entité à partir de l’id
-
On peut si on le souhaite trier nos données automatiquement (dans l’ordre de création par ex.)
Ainsi on peut simplifier les reducers
en supprimant tout ce qui est boilerplate, tout ce qui peut être source d’erreur :
// reducers.ts
// ...
export interface State {
//todos: Todo[];
todos: todoEntity.State; // (1)
filter: TodoFilter;
}
const initialState: State = {
//todos: [],
todos: todoEntity.initialState,
filter: 'SHOW_ALL',
};
export function reducer(
state: State = initialState,
action: fromTodos.TodoActionType,
): State {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
// todos: [
// ...state.todos,
// {
// text: action.text,
// completed: false,
// }
// ]
todos: todoEntity.adapter.addOne( // (1)
{
text: action.text,
creationDate: new Date(),
completed: false,
},
state.todos,
),
};
// ...
default: {
return state;
}
}
}
// ...
-
Mise à jour…
On manipulera des méthodes plus intuitives que de faire des map
ou des filter
:
-
todoEntity.adapter.addOne()
-
todoEntity.adapter.addAll()
-
todoEntity.adapter.updateOne()
-
todoEntity.adapter.removeMany()
-
…
NgRx Entity va nous fournir des selecteurs :
// selectors.ts
// ...
export const {
selectIds: getTodoIds, // (1)
selectEntities: getTodoEntities, // (2)
selectAll: getAllTodos, // (3)
selectTotal: getTotalTodos, // (4)
} = todoEntity.adapter.getSelectors(
createSelector(getTodoState, state => state.todos),
);
// ...
-
getTodoIds
: retourne le tableau des ids -
getTodoEntities
: retourne l’objet qui contient le dictionnaire de nostodos
-
getAllTodos
: retourne le tableau destodos
-
getTotalTodos
: retourne le total destodos
Et les sélecteurs suivants sont mis à jour :
// selectors.ts
// ...
export const getFilteredTodos = createSelector(
//getTodos,
getAllTodos, // (1)
getFilter,
(todos, filter) => {
switch (filter) {
// ...
}
},
);
export const getHasTodos = createSelector(getTotalTodos, totalTodos => {
//return todos.length > 0;
return totalTodos > 0; // (1)
});
// ...
-
Sélecteurs mis à jour…
📎
|
NgRx Router Store va permettre de gérer l’URL en le "bindant" au store, afin de récupérer l’état de l’URL.
On va pourvoir retrouver dans le store l’état de l’URL (url, path, params, queryParams, …) :
De nouvelles actions sont disponibles (ROUTER_NAVIGATION, …) auxquelles on peut s’abonner.
L’idée serait de faire évoluer le filtre : on ne passe plus exclusivement par le store pour le choix du filtre, mais par l’URL. On aurait ainsi :
On fait évoluer les routes :
// app.module.js
// ...
const routes: Routes = [
{ path: ':filter', component: TodoComponent }, // (1)
{ path: '**', redirectTo: 'all', pathMatch: 'full' },
];
// ...
-
On ajoute un paramètre
:filter
Et dans le store
on n’a plus besoin du filtre parce qu’il est géré dans le state
du router
:
// reducers.ts
// ...
export interface State {
todos: Todo[];
//filter: TodoFilter;
}
const initialState: State = {
todos: [],
//filter: 'SHOW_ALL',
};
En contre-partie on va créer un nouveau calculated selector
:
// selectors.ts
// ...
export const getFilter = createSelector(
fromApp.getRouterState, // (1)
(routerState): TodoFilter => {
switch (routerState.params.filter) { // (2)
case 'active': {
return 'SHOW_ACTIVE';
}
case 'completed': {
return 'SHOW_COMPLETED';
}
default: {
return 'SHOW_ALL';
}
}
},
);
-
On va chercher les données dans le
routerState
-
On lit le
params.filter
et retourne le typeTodoFilter
correspondant (SHOW_ACTIVE
,SHOW_COMPLETED
ouSHOW_ALL
)
On peut ensuite utiliser ce getFilter
dans les effets :
// effects.ts
// ...
@Effect({ dispatch: false })
filter$: Observable<Action> = this.actions$
.ofType(fromTodos.SET_TODO_FILTER) // (1)
.do((action: fromTodos.SetFilterAction) => {
switch (action.filter) {
case 'SHOW_ACTIVE': {
this.router.navigate(['/', 'active']); // (2)
break;
}
case 'SHOW_COMPLETED': {
this.router.navigate(['/', 'completed']); // (2)
break;
}
default: {
this.router.navigate(['/', 'all']); // (2)
break;
}
}
});
// ...
-
On écoute l’action
SET_TODO_FILTER
… -
… Et change l’URL en fonction
🔥
|
En observant les modifications apportées à cette étape (https://github.com/bbaia/gdgtoulouse-ngrx/compare/router-1…router-2), Bruno Baia fait noter le découplage de l’application. On a pu passer d’un mode sans URL à un mode avec URL, sans modifications des composants graphiques. Les composants graphiques sont donc vraiment "dumb" et le composant "smart" |
-
Vous n’êtes pas obligé d’utiliser Redux : voir l’article You Might Not Need Redux de Dan Abramov, Co-auteur de Redux, sur https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367.
-
Redux n’est pas nécessaire pour du CRUD, des applications de gestion ou de simple formulaire.
-
Redux devient nécessaire pour des applications collaboratives, interactives, avec des synchronisation avec des états distants, quand on veut du cache, des websockets, …
-
On sait qu’on peut avoir besoin de Redux quand on arrive à une taille critique de l’application où l’on se perd dans les modifications à apporter.
-
La courbe d’apprentissage est assez longue.
-
En contre partie les bugs sont facilement détectables.
-
On peut avoir une bonne expérience de développement car l’état est un simple objet JavaScript, tout comme les actions. Vous pouvez reproduire facilement les actions pour retrouver un état. Les outils et le timetravel permettent de bien inspecter et de debbuguer.
-
Redux s’applique extrêmement bien à Angular.