In a former article, I wrote an introduction for integrating ngrx/effects with Angular (+2) - a functional approach for reacting to a chain of events. Since then, based on this approach, I defined more side effects for several actions in my open source project, Echoes Player. It has really proven itself to be worthwhile in terms of logics architecture and code organization. In this post, I share more insights on working with ngrx/effects version 2 (currently in beta) and how show a real app use case where I applied it.
[UPDATED: October, 5th, 2016] This article’s focus is on the user profile component in Echoes Player. This component is rendered on the ”My Playlists” screen and its goal is to allow the user to sign in to the youtube account and display his/hers playlists.
This component involves several actions:
In order to achieve these, the user profile component uses theses services:
There are more actions in this component which require other services to be included:
We’re going to focus on the first 3 services which allow sign-in and displaying the user’s playlists.
Before I refactored the user profile service, it was responsible for few operations:
Obviously, the code structure can be better - I wanted to achieve a lot in a limited time and knowledge, and using these tools was just with a preliminary knowledge. I felt from the start that this implementation should be organized and authored in a different way. The UserProfile service achieved too much of its purpose.
And so as time passed by, I began refactoring this service and the relevant files around it. I realized that the user profile service can be regarded as actually a data provider service. It focus on getting the current authorized user profile related data.
Before we dive into the main refactoring of this service and its related effects, lets go through the responsibilities that have been taken out of this service:
The authorization process has been extracted to its own service in the app’s core services as well as the gapi (google api) loader.
GapiLoader is an observable service which can be subscribed in order to be notified once the desired google api has been loaded. In this case, I wanted to make a reusable service that can be agnostic to the api’s it loads while returning an observable. Google Api is a 3rd party api with a Promise base api. For that, I used the Observable Object to create a custom observable:
@Injectable()
export class GapiLoader {
private _api: Observable<any>
constructor() {}
load(api: string) {
return this.createApi(api)
}
private _loadApi(api: string, observer) {
const gapiAuthLoaded =
window.gapi && window.gapi.auth2 && window.gapi.auth2.getAuthInstance()
if (gapiAuthLoaded && gapiAuthLoaded.currentUser) {
return observer.next(gapiAuthLoaded)
}
window.gapi.load(api, response => observer.next(response))
}
private createApi(api) {
this._api = new Observable(observer => {
const isGapiLoaded = window.gapi && window.gapi.load
const onApiLoaded = () => this._loadApi(api, observer)
if (isGapiLoaded) {
onApiLoaded()
} else {
window["apiLoaded"] = onApiLoaded
}
})
return this._api
}
}
There’s some Typescript here. Clearly, aside from the constructor method, the only public method that is allowed to be used is the ”load” method, which takes an api name and simply returns an observable that the consumer can subscribe to.
The consumer of this service is the Authorization service. It is injected with the gapiLoader service, so it can load the relevant api for authorizing a google user - ‘auth2‘ api. Once the gapiLoader has loaded the auth2 api, the subscribed function is invoked, so it can continue to try and authorize the user. Even more than that, this function reacts to the gapiLoader’s observable stream. If at some point, the user signs out, this function should handle that. However, this is not regarded for now.
@Injectable()
export class Authorization {
private isSignedIn: boolean = false
private _googleAuth: any
constructor(
private zone: NgZone,
private store: Store<EchoesState>,
private gapiLoader: GapiLoader,
private userProfileActions: UserProfileActions
) {
this.loadAuth()
}
loadAuth() {
// attempt to SILENT authorize
this.gapiLoader.load("auth2").subscribe(authInstance => {
if (authInstance && authInstance.currentUser) {
return (this._googleAuth = authInstance)
}
this.authorize().then(GoogleAuth => {
const isSignedIn = GoogleAuth.isSignedIn.get()
const authResponse = GoogleAuth.currentUser.get()
const hasAccessToken = authResponse
.getAuthResponse()
.hasOwnProperty("access_token")
this._googleAuth = GoogleAuth
if (isSignedIn && hasAccessToken) {
this.zone.run(() => this.handleSuccessLogin(authResponse))
}
})
})
}
authorize() {
const authOptions = {
client_id: `${CLIENT_ID}.apps.googleusercontent.com`,
scope: "profile email https://www.googleapis.com/auth/youtube",
}
return window.gapi.auth2.init(authOptions)
}
}
I won’t go in detail of the code that runs in the “then” block, since, currently, google’s ‘auth2‘ api doesn’t allow to “silently” authenticate the user as it did in the previous version of ”auth” (at least, in the client side). so this code currently doesn’t trigger the signIn function (if you are familiar with a way to silently authenticate the user with auth2 - please do let me know in the comments or through the contact page). [UPDATED: October, 5th, 2016] I added the ”scope” entry to the authOptions in the ”authorize”. Now, if the user is already authenticated, the ”loadAuth” runs the ”handleSuccessLogin” and sends the authResponse as if the user clicked the sign-in button.
The signIn function is an important take out of the user profile service. It eliminated the code in the user profile service which attached a click handler for signing in the user (a copy paste from google’s guide). Once the signIn process has successfully been completed, the app needs to save the access token.
I use ngrx/store in order to dispatch an action for saving the token. Notice that I use NgZone to wrap the ”handleSuccessLogin” function handler since the google authorization signIn api is external to angular (I wrote about using NgZone for 3rd party external api before):
export class Authorization {
// ....
signIn() {
const run = fn => r => this.zone.run(() => fn.call(this, r))
const scope = "profile email https://www.googleapis.com/auth/youtube"
const signOptions = { scope }
if (this._googleAuth) {
this._googleAuth
.signIn(signOptions)
.then(run(this.handleSuccessLogin), run(this.handleFailedLogin))
}
}
handleSuccessLogin(response) {
const token = response.getAuthResponse().access_token
this.isSignedIn = true
this.store.dispatch(this.userProfileActions.updateToken(token))
}
//...
}
Now comes the fun part - using reactive programming with a declarative effects style. In the old code, the user profile service was responsible for saving the user’s access token, fetching the playlists and handle several calls for fetching more playlists of the amount si higher than 50 (google’s api limit).
I consider this flow to be a candidate for declaring this logic in an effects object - which is known as a side effect. As a reminder, a side effect usually should be declared when an action should be followed by another action. To simply put - this is a reactive system - the system should always apply 2 or more actions in a cascading order. With ngrx/effects we apply reactive programming in a declarative style.
For the first side effect, whenever the token is updated in the user profile store, this actions should follow:
These 3 actions are defined in the first effect:
@Injectable()
export class UserProfileEffects {
constructor(
private actions$: Actions,
private userProfileActions: UserProfileActions,
private userProfile: UserProfile
){}
@Effect() updateToken$ = this.actions$
.ofType(UserProfileActions.UPDATE_TOKEN)
.map(action => action.payload)
.map((token: string) => this.userProfile.setAccessToken(token))
.switchMap(string => this.userProfile.getPlaylists(true))
.map(response => this.userProfileActions.updateData(response));
...
}
The “updateData” method, saves a playlists response in store. This is the json response from the getPlaylist request:
{
"kind": "youtube#playlistListResponse",
"etag": "unique-id",
"nextPageToken": "a-unique-page-token",
"pageInfo": {
"totalResults": 54,
"resultsPerPage": 50
},
"items": [...]
}
Several actions should be taken upon this response arrival:
These 2 actions are decoupled and not connected to each other. In contrary to the effect that we declared for updating the token, these can be declared each in an effect. Separating them will allow to test them each on its own as well as invoke more actions, if needed:
export class UserProfileEffects {
...
@Effect() addUserPlaylists$ = this.actions$
.ofType(UserProfileActions.UPDATE)
.map(action => action.payload)
.map((data: any) =>this.userProfileActions.addPlaylists(data.items));
@Effect() updateNextPageToken$ = this.actions$
.ofType(UserProfileActions.UPDATE)
.map(action => action.payload)
.map(data => {
const nextPageToken = data.nextPageToken;
return nextPageToken
? this.userProfileActions.updatePageToken(data.nextPageToken)
: this.userProfileActions.userProfileCompleted();
});
...
}
The last action which updated the page token, does that with dispatching an event to the user profile store. In react to this action (remember reactive programming?), there is one more action that should be invoked: again ,getting the next page of playlists, if the page’s token value is a string of some kind.
The last effect in this saga, reacts to the update action of the page token:
export class UserProfileEffects {
...
@Effect() getMorePlaylists$ = this.actions$
.ofType(UserProfileActions.UPDATE_NEXT_PAGE_TOKEN)
.map(action => action.payload)
.switchMap((pageToken: string) => {
this.userProfile.updatePageToken(pageToken);
return this.userProfile.getPlaylists(false);
})
.map(response => this.userProfileActions.updateData(response));
}
The reactive system notion continues right after the last 3rd action. The 2 effects: addUserPlaylists$ and updateNextPageToken$ will be triggered again.
Eventually, this process will stop once there’s no page token in the response.
The operation which starts this whole process is clicking the sign-in button:
Upon successful authorization of the user, the chain of effects starts to react to the appropriate actions and finally, displays the user’s playlists.
Once again, ngrx/effects has proven to be a great declarative way for grouping side effects and taking out complex logics from services.
Organizing and declaring these effects, gives a great overview of how actions influence on other actions.
Echoes Player ng2 version is an open source application hosted on github. You can try the live version as well.