I've built a number of single page applications in Angular 2, 4, 6, and now 8 (for some reason I tend to like the even-numbered versions).
With 8, I've looked into NgRx and it's a significantly different experience. I want to share some early thoughts as I'm early on in this journey. This will be more about my reactions and less about the code, though I intend to follow-up on this later with a proper tutorial.
Angular Heroics
My prior experience in Angular revolved around:
- Breaking a page down into individual components needed for the UI
- Introducing a service for each major piece of application data needed as well as specialized services for things like logging, web communications, showing notifications, etc.
- Wiring things together with Routes and Child Routes
- Using Resolvers to pull data out of Services and provide them to Components based on the Route chosen
- Using Guards to prevent invalid use of Routes
Most Angular development involved adding Components or Services, then managing subscriptions in ngOnInit
and ngOnDestroy
and binding in the template. Dependency Injection made the world go round with easy access to services from components and things stayed simple and usable.
State strikes back
But things got complicated. In applications with client and server state, the user would make a change and you would then call out to the server to tell the server about the local change. If the server rejected the change, you had to update the UI. Meanwhile, the user could be performing other actions which resulted in other calls. Additionally, some subsequent calls could complete before their earlier calls have returned to the UI.
All of this brews a storm of opportunities for confusing state and communications-related bugs. I've actually resorted to recording video of my network tab mixed with the application UI in order to watch the state of the application and what operations caused the UI to get out of sorts.
NgRx to the Rescue?
NgRx, or Reactive Libraries for Angular, brings the Redux pattern of state management to Angular. The Redux pattern is designed as a state-oriented cycle where the UI dispatches actions that a reducer uses to update state, which then is dispatched back to the UI. The key factors here are that state is managed in a centralized spot, and only a handful of reducer actions can modify state, so if a bug occurs in state management, the culprit is easily identifiable.
Beyond that, the dev tools for Redux are fantastic.
The tools let you look at the history of your application state over time and even jump between different state versions and watch your UI update. This makes the complexities of debugging asynchronous state changes significantly easier.
The downside of this is that instead of dealing with services and methods for the majority of your actions, you have to think in a much more reactive manner, considering event streams, selectors, etc.
For example, to respond to a user event, instead of this:
public onBeginShiftClick(): void {
this.shiftService.beginShift();
}
you would do this:
public onBeginShiftClick(): void {
this.state.dispatch(beginShiftAction());
}
This gets dispatched to a reducer which looks like this:
const gameReducer = createReducer(GameSimulator.buildDefaultState(),
on(beginShiftAction, state => GameSimulator.simulate(state)),
// ....
);
Not too hard, I admit.
Where it gets very different is how you get data out of the redux store. Instead of doing something like:
@Component({
selector: 'ssit-crew-page',
templateUrl: './crew-page.component.html',
styleUrls: ['./crew-page.component.styl']
})
export class CrewPageComponent implements OnInit {
public crew: CrewMember[];
constructor(private crewService: CrewService) {
}
ngOnInit() {
this.crew = this.crewService.crew;
}
}
You would instead do:
@Component({
selector: 'ssit-crew-page',
templateUrl: './crew-page.component.html',
styleUrls: ['./crew-page.component.styl']
})
export class CrewPageComponent implements OnInit {
public crew$: Observable<CrewMember[]>;
constructor(private store: GameStateStore) {
}
ngOnInit() {
this.crew$ = this.store.select(this.store.getCrewMembers);
}
}
Because you're now working with an observable stream, you also have to remember to use the async pipe (e.g. crew | async
) in your template so that the event is auto-subscribed / unsubscribed.
Additionally, you need to implement the selector in the state store:
const gameSelector = createFeatureSelector<GameState>('game');
export class GameStateStore extends Store<GameState> {
public readonly getCrewMembers = createSelector(gameSelector, (state: GameState) => state.crew);
// ...
}
Closing Thoughts
Overall, I'm very early on in my NgRx journey. There are definite advantages and disadvantages so it's not for every project.
Pros
- Creates a very clear audit trail of state changes
- State changes occur in isolated locations
- No need to subscribe / unsubscribe from events from services (technically, these could be handled with observable streams and pipes, but historically I've subscribed so I'm bundling the new behavior in with NgRx)
Cons
- More complex than working with individual services
- Selector syntax can be hard to interpret
- Async pipes can be cumbersome and muddle up your templates
- My development speed is much lower, but a lot of that is due to constant learning a different flavor of Angular
I'm going to keep working on NgRx and Angular with my learning project. I'll keep you posted.
Top comments (6)
Angular is 2 way data binding, so the states of the variables are always available in real time without having to code anything. This is the big interest of Angular. And the stores are native in Angular : they are made with a simple native service. Adding Redux to Angular is not only useless, but it will destroy the standard functionning of Angular, and cause a dramatic recoding of the 2 way data binding with monstruous problems to deal with the asynchronism. Please DO NOT do that.
What's your recommendation for handling complex async state changes in Angular?
Angular is 2 way data binding, so all the changes of all variables are natively watched.
Therefore I make a store with a service, and I call directly this store inside the HTML templates of the component, and so I have no local variable to declare in the current component. The amount of code for each component is drastically lowered.
Any change of a variable in such a store, performed from the typescript or from an input of the HTML template, is automatically applied to all the application (depending upon the module where you imported your store service, see singleton of your store).
Nearly everything is async in Javascript (events, call to server, indexedDB, ....) so Angular is fundamentally designed to deal with this asynchronism. You only need to use the Elvis operator (myvar?.myproperty) for async variables in the template of the component, and initiate your component into a NgOnInit function, or ngAfterViewInit in some case. This is because initiating your component in the constructor will not be suitable with asynchroneous processes (see life cycle). The constructor is just used to import the store service.
All this is working tremendously well, and morever for very rich and complex applications (see for instance oceanvirtuel.eu).
You should try Akita. github.com/datorama/akita
Thank you! That was not on my radar. It doesn't look (from a quick skim) that it offers the same sort of debugging experience that Redux offers, but it does offer a simplified state management experience that's one step up from manually implementing it in services yourself.
It does. It comes with Redux devtools (netbasal.gitbook.io/akita/enhancer...) support, and plugins that you will not find in ngrx such as server side pagination, dirty checking, forms manager, etc. If you are coming from .NET this will be more natural to you as it's OOP concepts.