For your Angular
app
Maxim Salnikov
@webmaxru
Reactive State Management
How to build fast, robust, predictable data-driven apps
And enjoy doing this
Maxim Salnikov
-
Google Developer Expert in Angular
-
Mobile Oslo / Angular Oslo / PWA Oslo meetups organizer
-
ngVikings conference organizer
Products from the future
UI Engineer at ForgeRock
ngVikings.org
-
All sides of Angular ecosystem
-
100% community-driven event from developers for developers
-
True Nordics spirit and Vikings power
Follow Twitter
March 1-2, Helsinki, Finland
Why?
3
3
Increasing complexity
Model
Model
View
View
View
Unidirectional
Action
Dispatcher
Store
View
Action
Maintains
app state
State complexity
-
Server responses
-
Cached data
-
Real-time data feeds
-
Locally created data
-
UI state
3
Redux
Redux is simple
slim-redux.js: 99-liner
Flow
Action
Store
View
Reducer
interaction
dispatch action
new state
state, action
current state
State
A plain object without setters representing entire state of an app
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
Action
A plain object that represents an intention to change the state. Actions are the only way to get data into the store.
{ type: 'ADD_MESSAGE', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
Reducer
A pure function that accepts a state and an action and returns a new state.
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map(
(todo, index) =>
action.index === index
? { text: todo.text, completed: !todo.completed }
: todo
)
default:
return state
}
}
Main principles
Single source of truth
1.
Main principles
Single source of truth
State is read-only
1.
2.
Main principles
Changes are made with pure functions
Single source of truth
State is read-only
1.
2.
3.
Immutability?
Debugging
Performance
Coherence
1.
2.
3.
What about
?
reactjs / redux
angular-redux / store
ngrx / store
=
+
=
Why ngrx/store?
Use AsyncPipe in the templates
Staying consistent with the Angular architecture
Applying reactive middleware
1.
2.
3.
Demo app
MessageStatus
MessageSection
MessageList
MessageForm
Header
App
Demo app
MessageToolbar
MessageStatus
MessageSection
MessageList
MessageForm
Header
App
Demo app
MessageToolbar
MessageStatus
MessageSection
MessageList
MessageForm
Header
App
Demo app
MessageToolbar
MessageStatus
MessageList
MessageForm
App
State Management Container
MessageToolbar
State
State
export interface State {
messages: MessageReducer.State,
ui: UiReducer.State;
}
export interface State {
items: Message[],
searchQuery: string
}
const initialState: State = {
items: [],
searchQuery: 'ngrx'
};
export interface State {
testPanelOpen: boolean
}
const initialState: State = {
testPanelOpen: false
};
Actions
import { Action } from '@ngrx/store';
export const CLOSE_TEST_PANEL = '[UI] Close Test Panel';
export const OPEN_TEST_PANEL = '[UI] Open Test Panel';
export class CloseTestPanel implements Action {
readonly type = CLOSE_TEST_PANEL;
}
...
export type All = CloseTestPanel | OpenTestPanel;
Action with payload
export class Add implements Action {
readonly type = ADD;
constructor(public payload: Message) { }
}
export class SearchSuccess implements Action {
readonly type = SEARCH_SUCCESS;
constructor(public payload: Message[]) { }
}
Reducer
export function reducer(state = initialState, action: action): State {
switch(action.type) {
case MessageActions.ADD: {
return ...
}
case MessageActions.SEARCH: {
return ...
}
case MessageActions.SEARCH_SUCCESS: {
return ...
}
default: {
return state;
}
}
}
Reducer
case MessageActions.SEARCH_SUCCESS: {
return {
...state,
items: state.items.concat(action.payload)
};
}
Only non-mutating methods!
- copyWithin
- fill
- pop
- push
- reverse
- shift
- sort
- splice
- unshift
- ...
- concat
- slice
- filter
- map
VS
Wiring up all together
export const ROOT_REDUCER = {
messages: MessageReducer.reducer,
ui: UiReducer.reducer
};
import { StoreModule } from '@ngrx/store';
import { ROOT_REDUCER } from './states/reducers';
...
@NgModule({
imports: [
StoreModule.provideStore(ROOT_REDUCER),
...
],
...
})
Injecting the store
import { Store } from '@ngrx/store';
import * as ApplicationStore from './../states/reducers';
...
export class MessageStatusComponent implements OnInit {
messagesNumber$: Observable<number>
constructor(private store: Store<ApplicationStore.State>) {
this.messagesNumber$ =
this.store.select(store => store.messages.items.length);
}
}
{{ messagesNumber$ | async }}
Creating selectors
this.store.select(store => store.messages.items.length)
this.store.select(ApplicationStore.selectMessagesNumber)
export function selectMessagesNumber(state: State) {
return state.messages.items.length
}
Dispatching an action
this.store.dispatch({
type: MessageActions.ADD,
payload: {
author: messageForm.value.author,
text: messageForm.value.text,
createdAt: new Date()
}
});
Async action
searchMessages(searchString: string) {
this.store.dispatch( ...SEARCH... );
this.messageService.searchMessages(searchString)
.subscribe(
messages => {
this.store.dispatch( ...SEARCH_SUCCESS... );
});
}
constructor(private messageService: MessageService) { }
ngrx / effects
Isolating side-effects from components
All interaction with outside world: HTTP, WebSockets,
Storage, etc
Centralizing all asynchronous work
1.
2.
3.
Creating effect
@Injectable()
export class MessageEffects {
@Effect()
search$: Observable<Action> = this.actions$.ofType(MessageActions.SEARCH)
.map((action: MessageActions.Search) => action.payload)
.switchMap(searchQuery => this.messageService.searchMessages(searchQuery))
.map(results => new MessageActions.SearchSuccess(results));
constructor(private actions$: Actions, private messageService: MessageService)
{}
}
import { Effect, Actions } from '@ngrx/effects';
Actions for effects
{ type: 'SEARCH' }
{ type: 'SEARCH_SUCCESS', items: { ... } }
{ type: 'SEARCH_FAILURE', error: 'Oops' }
Action
Effect
Action Success
Action Failure
Running effect
import { EffectsModule } from '@ngrx/effects';
import { MessageEffects } from './states/message.effects';
...
@NgModule({
imports: [
EffectsModule.run(MessageEffects),
...
],
...
})
searchMessages(searchString: string) {
this.store.dispatch({
type: MessageActions.SEARCH,
payload: searchString
});
}
Redux DevTools
Live debugging & much more...
Time Traveling (really!)
Save and Restore state
1.
2.
3.
Redux DevTools
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
...
@NgModule({
imports: [
storeDevtoolsModule.instrumentOnlyWithExtension({
maxAge: 5
}),
...
],
...
})
Redux DevTools
Out of today's scope
Many other ngrx features. Stay tuned!
ngrx/router-store to connect with Angular router
Composing Reducers to apply Middleware
1.
2.
3.
Tips & Tricks
Use ngrx-store-freeze to ensure
immutability during development
Use reactjs/reselect to create efficient Selectors
Use CLI's environment feature to conditionally wire up store/devtools
1.
2.
3.
To sum up
More boilerplate code and some
architectural limitations
Great performance
Awesome developer experience
1.
2.
3.
The decision is yours!
תודה רבה לך
@webmaxru
Maxim Salnikov
Questions?
Reactive State Management For Your Angular App
By Maxim Salnikov
Reactive State Management For Your Angular App
Your Angular application is a reactive system. It reacts to different events and updates the model, then propagates the changes through the component tree. It works like a charm for the simple and non UI-intensive apps. But when you, following this architecture, meet some more complex usecases (like concurrent data modification, complex component intercommunications, the need to keep temporary UI state, etc), it's time to think about different way to manage the app state. Redux pattern to the rescue! But Redux in Angular way. We have reactive forms, reactive router, observables-based http-client, so let's have a look at reactive Redux called ngrx/store. In this session: intro to the state management (famous Facebook bug), base principles and components of Redux, converting our regular app to the one using centralized store using ngrx/store, tooling, pros and cons of having centralized store for the state management.
- 4,291