Angular 2
ngrx
Recherche…
Introduction
Exemple complet: Connexion / Déconnexion d'un utilisateur
Conditions préalables
Ce sujet ne concerne pas Redux et / ou Ngrx:
- Vous devez être à l'aise avec Redux
- Au moins comprendre les bases de RxJs et le motif observable
Tout d'abord, définissons un exemple dès le début et jouons avec du code:
En tant que développeur, je veux:
- Avoir une interface
IUser
qui définit les propriétés d'unUser
- Déclarez les actions que nous utiliserons plus tard pour manipuler l'
User
dans leStore
- Définir l'état initial du
UserReducer
- Créer le réducteur
UserReducer
- Importez notre
UserReducer
dans notre module principal pour créer leStore
- Utilisez les données du
Store
pour afficher des informations à notre vue
Alerte Spoiler : Si vous voulez essayer la démo immédiatement ou lire le code avant même de commencer, voici une vue Plunkr ( vue d' intégration ou exécution ).
1) Définir IUser
Interface
J'aime partager mes interfaces en deux parties:
- Les propriétés que nous obtiendrons d'un serveur
- Les propriétés que nous définissons uniquement pour l'interface utilisateur (si un bouton tourne par exemple)
Et voici l'interface IUser
nous utiliserons:
user.interface.ts
export interface IUser {
// from server
username: string;
email: string;
// for UI
isConnecting: boolean;
isConnected: boolean;
};
2) Déclarez les actions pour manipuler l' User
Maintenant, nous devons réfléchir au type d'actions que nos réducteurs sont censés gérer.
Disons ici:
user.actions.ts
export const UserActions = {
// when the user clicks on login button, before we launch the HTTP request
// this will allow us to disable the login button during the request
USR_IS_CONNECTING: 'USR_IS_CONNECTING',
// this allows us to save the username and email of the user
// we assume those data were fetched in the previous request
USR_IS_CONNECTED: 'USR_IS_CONNECTED',
// same pattern for disconnecting the user
USR_IS_DISCONNECTING: 'USR_IS_DISCONNECTING',
USR_IS_DISCONNECTED: 'USR_IS_DISCONNECTED'
};
Mais avant d'utiliser ces actions, laissez-moi vous expliquer pourquoi nous allons avoir besoin d'un service pour nous envoyer certaines de ces actions:
Disons que nous voulons connecter un utilisateur. Nous allons donc cliquer sur un bouton de connexion et voici ce qui va se passer:
- Cliquez sur le bouton
- Le composant intercepte l'événement et appelle
userService.login
-
userService.login
méthodeuserService.login
dispatch
un événement pour mettre à jour notre propriété store:user.isConnecting
- Un appel HTTP est déclenché (nous utiliserons un
setTimeout
dans la démo pour simuler le comportement asynchrone ) - Une fois l'appel
HTTP
terminé, nous enverrons une autre action pour avertir notre magasin qu'un utilisateur est connecté.
user.service.ts
@Injectable()
export class UserService {
constructor(public store$: Store<AppState>) { }
login(username: string) {
// first, dispatch an action saying that the user's tyring to connect
// so we can lock the button until the HTTP request finish
this.store$.dispatch({ type: UserActions.USR_IS_CONNECTING });
// simulate some delay like we would have with an HTTP request
// by using a timeout
setTimeout(() => {
// some email (or data) that you'd have get as HTTP response
let email = `${username}@email.com`;
this.store$.dispatch({ type: UserActions.USR_IS_CONNECTED, payload: { username, email } });
}, 2000);
}
logout() {
// first, dispatch an action saying that the user's tyring to connect
// so we can lock the button until the HTTP request finish
this.store$.dispatch({ type: UserActions.USR_IS_DISCONNECTING });
// simulate some delay like we would have with an HTTP request
// by using a timeout
setTimeout(() => {
this.store$.dispatch({ type: UserActions.USR_IS_DISCONNECTED });
}, 2000);
}
}
3) Définir l’état initial du UserReducer
user.state.ts
export const UserFactory: IUser = () => {
return {
// from server
username: null,
email: null,
// for UI
isConnecting: false,
isConnected: false,
isDisconnecting: false
};
};
4) Créez le réducteur UserReducer
Un réducteur prend 2 arguments:
- L'état actuel
- Une
Action
de typeAction<{type: string, payload: any}>
Rappel: un réducteur doit être initialisé à un moment donné
Comme nous avons défini l'état par défaut de notre réducteur dans la partie 3), nous pourrons l'utiliser comme ça:
user.reducer.ts
export const UserReducer: ActionReducer<IUser> = (user: IUser, action: Action) => {
if (user === null) {
return userFactory();
}
// ...
}
Heureusement, il existe un moyen plus simple d’écrire cela en utilisant notre fonction factory
pour renvoyer un objet et, dans le réducteur, utilisez une valeur de paramètres par défaut (ES6):
export const UserReducer: ActionReducer<IUser> = (user: IUser = UserFactory(), action: Action) => {
// ...
}
Ensuite, nous devons gérer toutes les actions de notre réducteur: CONSEIL : utilisez la fonction ES6 Object.assign
pour garder notre état immuable
export const UserReducer: ActionReducer<IUser> = (user: IUser = UserFactory(), action: Action) => {
switch (action.type) {
case UserActions.USR_IS_CONNECTING:
return Object.assign({}, user, { isConnecting: true });
case UserActions.USR_IS_CONNECTED:
return Object.assign({}, user, { isConnecting: false, isConnected: true, username: action.payload.username });
case UserActions.USR_IS_DISCONNECTING:
return Object.assign({}, user, { isDisconnecting: true });
case UserActions.USR_IS_DISCONNECTED:
return Object.assign({}, user, { isDisconnecting: false, isConnected: false });
default:
return user;
}
};
5) Importer notre UserReducer
dans notre module principal pour construire le Store
app.module.ts
@NgModule({
declarations: [
AppComponent
],
imports: [
// angular modules
// ...
// declare your store by providing your reducers
// (every reducer should return a default state)
StoreModule.provideStore({
user: UserReducer,
// of course, you can put as many reducers here as you want
// ...
}),
// other modules to import
// ...
]
});
6) Utiliser les données de la Store
pour afficher des informations à notre avis
Tout est maintenant prêt sur le côté logique et nous devons simplement afficher ce que nous voulons en deux composants:
-
UserComponent
: [Composant Dumb] Nous allons simplement passer l'objet utilisateur du magasin en utilisant la propriété@Input
et le tubeasync
. De cette façon, le composant ne recevra l'utilisateur qu'une fois qu'il sera disponible (et l'user
sera de typeIUser
et non de typeObservable<IUser>
!) -
LoginComponent
[Composant intelligent] Nous injecterons directement leStore
dans ce composant et travaillerons uniquement sur l'user
en tantObservable
.
user.component.ts
@Component({
selector: 'user',
styles: [
'.table { max-width: 250px; }',
'.truthy { color: green; font-weight: bold; }',
'.falsy { color: red; }'
],
template: `
<h2>User information :</h2>
<table class="table">
<tr>
<th>Property</th>
<th>Value</th>
</tr>
<tr>
<td>username</td>
<td [class.truthy]="user.username" [class.falsy]="!user.username">
{{ user.username ? user.username : 'null' }}
</td>
</tr>
<tr>
<td>email</td>
<td [class.truthy]="user.email" [class.falsy]="!user.email">
{{ user.email ? user.email : 'null' }}
</td>
</tr>
<tr>
<td>isConnecting</td>
<td [class.truthy]="user.isConnecting" [class.falsy]="!user.isConnecting">
{{ user.isConnecting }}
</td>
</tr>
<tr>
<td>isConnected</td>
<td [class.truthy]="user.isConnected" [class.falsy]="!user.isConnected">
{{ user.isConnected }}
</td>
</tr>
<tr>
<td>isDisconnecting</td>
<td [class.truthy]="user.isDisconnecting" [class.falsy]="!user.isDisconnecting">
{{ user.isDisconnecting }}
</td>
</tr>
</table>
`
})
export class UserComponent {
@Input() user;
constructor() { }
}
login.component.ts
@Component({
selector: 'login',
template: `
<form
*ngIf="!(user | async).isConnected"
#loginForm="ngForm"
(ngSubmit)="login(loginForm.value.username)"
>
<input
type="text"
name="username"
placeholder="Username"
[disabled]="(user | async).isConnecting"
ngModel
>
<button
type="submit"
[disabled]="(user | async).isConnecting || (user | async).isConnected"
>Log me in</button>
</form>
<button
*ngIf="(user | async).isConnected"
(click)="logout()"
[disabled]="(user | async).isDisconnecting"
>Log me out</button>
`
})
export class LoginComponent {
public user: Observable<IUser>;
constructor(public store$: Store<AppState>, private userService: UserService) {
this.user = store$.select('user');
}
login(username: string) {
this.userService.login(username);
}
logout() {
this.userService.logout();
}
}
Comme Ngrx
est une fusion des concepts de Redux
et de RxJs
, il peut être assez difficile de comprendre les détails au début. Mais il s'agit d'un modèle puissant qui vous permet, comme nous l'avons vu dans cet exemple, de disposer d'une application réactive et de partager facilement vos données. N'oubliez pas qu'il y a un Plunkr disponible et que vous pouvez le préparer pour faire vos propres tests!
J'espère que c'était utile même si le sujet est assez long, bravo!