ngrx
Introduction#
Ngrx is a powerful library that you can use with Angular2.
The idea behind is to merge two concepts that plays well together to have a reactive app with a predictable state container :
The main advantages :
-
Sharing data in your app between your components is going to easier
-
Testing your app core logic consists to test pure functions, without any dependency on Angular2 (very easy so !)
Complete example : Login/logout a user
Prerequisites
This topic is not about Redux and/or Ngrx :
- You need to be comfortable with Redux
- At least understand the basics of RxJs and Observable pattern
First, let’s define an example from the very beginning and play with some code :
As a developer, I want to :
- Have an
IUser
interface that defines the properties of aUser
- Declare the actions that we’ll use later to manipulate the
User
in theStore
- Define the initial state of the
UserReducer
- Create the reducer
UserReducer
- Import our
UserReducer
into our main module to build theStore
- Use data from the
Store
to display information in our view
Spoiler alert : If you want to try the demo right away or read the code before we even get started, here’s a Plunkr (embed view or run view).
1) Define IUser
interface
I like to split my interfaces in two parts :
- The properties we’ll get from a server
- The properties we define only for the UI (should a button be spinning for example)
And here’s the interface IUser
we’ll be using :
user.interface.ts
export interface IUser {
// from server
username: string;
email: string;
// for UI
isConnecting: boolean;
isConnected: boolean;
};
2) Declare the actions to manipulate the User
Now we’ve got to think about what kind of actions our reducers are supposed to handle.
Let say here :
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'
};
But before we use those actions, let me explain why we’re going to need a service to dispatch some of those actions for us :
Let say that we want to connect a user. So we’ll be clicking on a login button and here’s what’s going to happen :
- Click on the button
- The component catch the event and call
userService.login
userService.login
methoddispatch
an event to update our store property :user.isConnecting
- An HTTP call is fired (we’ll use a
setTimeout
in the demo to simulate the async behaviour) - Once the
HTTP
call is finished, we’ll dispatch another action to warn our store that a user is logged
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) Define the initial state of the UserReducer
user.state.ts
export const UserFactory: IUser = () => {
return {
// from server
username: null,
email: null,
// for UI
isConnecting: false,
isConnected: false,
isDisconnecting: false
};
};
4) Create the reducer UserReducer
A reducer takes 2 arguments :
- The current state
- An
Action
of typeAction<{type: string, payload: any}>
Reminder : A reducer needs to be initialized at some point
As we defined the default state of our reducer in part 3), we’ll be able to use it like that :
user.reducer.ts
export const UserReducer: ActionReducer<IUser> = (user: IUser, action: Action) => {
if (user === null) {
return userFactory();
}
// ...
}
Hopefully, there’s an easier way to write that by using our factory
function to return an object and within the reducer use an (ES6) default parameters value :
export const UserReducer: ActionReducer<IUser> = (user: IUser = UserFactory(), action: Action) => {
// ...
}
Then, we need to handle every actions in our reducer :
TIP: Use ES6 Object.assign
function to keep our state immutable
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) Import our UserReducer
into our main module to build the 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) Use data from the Store
to display information in our view
Everything is now ready on logic side and we just have to display what we want in two components :
UserComponent
: [Dumb component] We’ll just pass the user object from the store using@Input
property andasync
pipe. This way, the component will receive the user only once it’s available (and theuser
will be of typeIUser
and not of typeObservable<IUser>
!)LoginComponent
[Smart component] We’ll directly inject theStore
into this component and work only onuser
as anObservable
.
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();
}
}
As Ngrx
is a merge of Redux
and RxJs
concepts, it can be quite hard to understand the ins an outs at the beginning. But this is a powerful pattern that allows you as we’ve seen in this example to have a reactive app and were you can easily share your data. Don’t forget that there’s a Plunkr available and you can fork it to make your own tests !
I hope it was helpful even tho the topic is quite long, cheers !