Maxim Salnikov
@webmaxru
How to build fast, robust, predictable data-driven apps
Products from the future
UI Engineer at ForgeRock
3
3
Model
Model
View
View
View
Action
Dispatcher
Store
View
Action
Maintains
app state
3
Action
Store
View
Reducer
interaction
dispatch action
new state
state, action
current 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'
}
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' }
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
}
}
1.
1.
2.
1.
2.
3.
Debugging
Performance
Coherence
1.
2.
3.
1.
2.
3.
MessageStatus
MessageSection
MessageList
MessageForm
Header
App
MessageToolbar
MessageStatus
MessageSection
MessageList
MessageForm
Header
App
MessageToolbar
MessageStatus
MessageSection
MessageList
MessageForm
Header
App
MessageToolbar
MessageStatus
MessageList
MessageForm
App
MessageToolbar
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
};
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;
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[]) { }
}
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;
}
}
}
case MessageActions.SEARCH_SUCCESS: {
return {
...state,
items: state.items.concat(action.payload)
};
}
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),
...
],
...
})
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 }}
this.store.select(store => store.messages.items.length)
this.store.select(ApplicationStore.selectMessagesNumber)
export function selectMessagesNumber(state: State) {
return state.messages.items.length
}
this.store.dispatch({
type: MessageActions.ADD,
payload: {
author: messageForm.value.author,
text: messageForm.value.text,
createdAt: new Date()
}
});
searchMessages(searchString: string) {
this.store.dispatch( ...SEARCH... );
this.messageService.searchMessages(searchString)
.subscribe(
messages => {
this.store.dispatch( ...SEARCH_SUCCESS... );
});
}
constructor(private messageService: MessageService) { }
1.
2.
3.
@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';
{ type: 'SEARCH' }
{ type: 'SEARCH_SUCCESS', items: { ... } }
{ type: 'SEARCH_FAILURE', error: 'Oops' }
Action
Effect
Action Success
Action Failure
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
});
}
1.
2.
3.
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
...
@NgModule({
imports: [
storeDevtoolsModule.instrumentOnlyWithExtension({
maxAge: 5
}),
...
],
...
})
1.
2.
3.
1.
2.
3.
1.
2.
3.