Diese Präsentation wurde erfolgreich gemeldet.
Wir verwenden Ihre LinkedIn Profilangaben und Informationen zu Ihren Aktivitäten, um Anzeigen zu personalisieren und Ihnen relevantere Inhalte anzuzeigen. Sie können Ihre Anzeigeneinstellungen jederzeit ändern.

Gerenciamento de estado no Angular com NgRx

675 Aufrufe

Veröffentlicht am

Palestra apresentada no Meetup AngularSP no dia 28 de Setembro de 2017 na Google Brasil.

Veröffentlicht in: Technologie

Gerenciamento de estado no Angular com NgRx

  1. 1. Gerenciamento de estado no Angular com NgRx
  2. 2. Loiane Groner @loiane github.com/loiane loiane.com loiane.training Java, JavaScript/HTML5, Sencha, Angular, Phonegap/ Ionic
  3. 3. Disponível (inglês) na amazon.com.br
  4. 4. https://novatec.com.br/livros/estruturas-de-dados-algoritmos-em-javascript/
  5. 5. Entender como desenvolver projetos reativos com Angular
  6. 6. Entender como desenvolver projetos reativos com Angular #itsJustAngular
  7. 7. Programação eativa
  8. 8. http://www.reactivemanifesto.org/pt-BR Manifesto Reativo
  9. 9. Reagir a informações
  10. 10. Reagir a usuários
  11. 11. Reagir a erros
  12. 12. Reage a dados que são transmitidos ao longo do tempo
  13. 13. Extensões reativas (RxJS) http://reactivex.io/
  14. 14. Objeto Promise Iterable Observable Síncrono Assíncrono ValorúnicoMúltiplosvalores Panorama da teoria da reatividade
  15. 15. Padrões eativos
  16. 16. Gerenciar o estado e controle de fluxo
  17. 17. COMPONENTE {…} TEMPLATE <..>
  18. 18. COMPONENTE {…} TEMPLATE <..> Binding de Propriedades
  19. 19. COMPONENTE {…} TEMPLATE <..> Binding de Propriedades Binding de Eventos
  20. 20. COMPONENTE {…} TEMPLATE <..> DIRETIVAS {..} Binding de Propriedades Binding de Eventos
  21. 21. COMPONENTE {…} TEMPLATE <..> DIRETIVAS {..} SERVIÇOS SERVIÇO A SERVIÇO B Binding de Propriedades Binding de Eventos
  22. 22. COMPONENTE {…} TEMPLATE <..> DIRETIVAS {..} SERVIÇOS SERVIÇO A SERVIÇO B MÓDULO X MÓDULO A MÓDULO B Binding de Propriedades Binding de Eventos
  23. 23. Yay! Projeto novo em Angular!
  24. 24. COMPONENTE {…} SERVIÇO
  25. 25. COMPONENTE {…} SERVIÇO COMPONENTE {…} SERVIÇO COMPONENTE {…} SERVIÇO COMPONENTE {…} SERVIÇO
  26. 26. uma semana depois…
  27. 27. @Component({ selector: 'app-product-order', templateUrl: './product-order.component.html', styleUrls: ['./product-order.component.scss'] }) export class ProductOrderComponent implements OnInit { constructor( private router: Router, private route: ActivatedRoute, private location: Location, private productService: ProductService, private clientService: ClientService, private addressService: AddressService, private userRoleService: UserRoleService, private comboboxService: ComboboxService, private exportFileService: ExportPDFService ) {} }
  28. 28. COMPONENTE {…} SERVIÇO COMPONENTE {…} COMPONENTE {…} COMPONENTE {…} SERVIÇO SERVIÇO SERVIÇO SERVIÇO COMPONENTE {…} SERVIÇO
  29. 29. @Injectable() export class AuthService { private loggedIn = new BehaviorSubject<boolean>(false); get isLoggedIn() { return this.loggedIn.asObservable(); } login(user: User){ if (valid.userName !== '' && user.password != ‘') { this.loggedIn.next(true); } } logout(){ this.loggedIn.next(false); } }
  30. 30. O maior problema no desenvolvimento e manutenção de sistemas de software de grande escala é a complexidade - sistemas grandes são difíceis de entender Ben Moseley & Peter Marks Out of the Tar Pit: Analysis of Software Complexity
  31. 31. Acreditamos que o principal contribuinte para esta complexidade em muitos sistemas é o gerenciamento do estado e o fardo que isso acrescenta ao tentar analisar e entender o sistema. Outros contribuintes estreitamente relacionados são o volume do código e a preocupação com o controle de fluxo do sistema. Ben Moseley & Peter Marks Out of the Tar Pit: Analysis of Software Complexity
  32. 32. Redux é uma biblioteca, e também é um padrão
  33. 33. Redux é uma biblioteca, e também é um padrão reativo
  34. 34. sem Redux Componente iniciando a mudança
  35. 35. Não reativo Variável comum <number>
  36. 36. Não reativo
  37. 37. Estado com Redux
  38. 38. Estado Redux Store Ação dispatch subscribe
  39. 39. Passo 1: Definir actions export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export const RESET = 'RESET';
  40. 40. Passo 2: Definir estado inicial e reducer export const counterReducer: ActionReducer<number> = ( state: number = 0, action: Action ) => { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } };
  41. 41. Passo 2: Definir estado inicial e reducer export const counterReducer: ActionReducer<number> = ( state: number = 0, action: Action ) => { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } }; Recebe o estado inicial
  42. 42. Passo 2: Definir estado inicial e reducer export const counterReducer: ActionReducer<number> = ( state: number = 0, action: Action ) => { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } }; Recebe o estado inicial Recebe a ação + payload
  43. 43. Passo 2: Definir estado inicial e reducer export const counterReducer: ActionReducer<number> = ( state: number = 0, action: Action ) => { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } }; Recebe o estado inicial Recebe a ação + payload Retorna novo estado baseado na ação
  44. 44. reducer Novo Estado Estado Ação dispatch + store
  45. 45. Flow de dados unidirecional com Redux Reducer Store Component Ações subscribe dispatch
  46. 46. https://github.com/ngrx
  47. 47. Flow de dados unidirecional com NgRx Reducer Component subscribe dispatch Store Ações
  48. 48. import { StoreModule } from '@ngrx/store'; import { counterReducer } from './reducers/counter'; @NgModule({ imports: [ StoreModule.provideStore({ counter: counterReducer }) ] }) export class AppModule { } Reducers Globais
  49. 49. Store API // seleciona todo o estado this.store.select(state => state); // seleciona parte do estado this.store.select(state => state.tasks); this.store.select('tasks'); // dispara ações this.store.dispatch({ type: 'ACTION_TYPE', payload: {...} });
  50. 50. Store API // seleciona todo o estado this.store.select(state => state); // seleciona parte do estado this.store.select(state => state.tasks); this.store.select('tasks'); // dispara ações this.store.dispatch({ type: 'ACTION_TYPE', payload: {...} });
  51. 51. Store API // seleciona todo o estado this.store.select(state => state); // seleciona parte do estado this.store.select(state => state.tasks); this.store.select('tasks'); // dispara ações this.store.dispatch({ type: 'ACTION_TYPE', payload: {...} });
  52. 52. @Component({ template: ` <div>Counter: {{ counter }}</div> ` }) export class CounterComponent { counter: number; counterSub: Subscription; ngOnInit() { this.counterSub = Observable.interval(1000) .startWith(0) .subscribe(counter => (this.counter = counter)); } ngOnDestroy() { this.counterSub.unsubscribe(); } }
  53. 53. @Component({ template: ` <div>Counter: {{ counter }}</div> ` }) export class CounterComponent { counter: number; counterSub: Subscription; ngOnInit() { this.counterSub = Observable.interval(1000) .startWith(0) .subscribe(counter => (this.counter = counter)); } ngOnDestroy() { this.counterSub.unsubscribe(); } }
  54. 54. @Component({ template: ` <div>Counter: {{ counter }}</div> ` }) export class CounterComponent { counter: number; counterSub: Subscription; ngOnInit() { this.counterSub = Observable.interval(1000) .startWith(0) .subscribe(counter => (this.counter = counter)); } ngOnDestroy() { this.counterSub.unsubscribe(); } }
  55. 55. @Component({ template: ` <div>Counter: {{ counter }}</div> ` }) export class CounterComponent { counter: number; counterSub: Subscription; ngOnInit() { this.counterSub = Observable.interval(1000) .startWith(0) .subscribe(counter => (this.counter = counter)); } ngOnDestroy() { this.counterSub.unsubscribe(); } }
  56. 56. @Component({ template: ` <div>Counter: {{ counter }}</div> ` }) export class CounterComponent { counter: number; counterSub: Subscription; ngOnInit() { this.counterSub = Observable.interval(1000) .startWith(0) .subscribe(counter => (this.counter = counter)); } ngOnDestroy() { this.counterSub.unsubscribe(); } }
  57. 57. http://tradingadvantagedaily.com/wp-content/uploads/2017/07/1c00898.jpg
  58. 58. @Component({ template: `Counter: {{ counter$ | async }}` }) export class CounterComponent { counter$: Observable<number>; ngOnInit() { this.counter$ = Observable.interval(1000).startWith(0); } }
  59. 59. @Component({ template: `Counter: {{ counter$ | async }}` }) export class CounterComponent { counter$: Observable<number>; ngOnInit() { this.counter$ = Observable.interval(1000).startWith(0); } }
  60. 60. @Component({ template: `Counter: {{ counter$ | async }}` }) export class CounterComponent { counter$: Observable<number>; ngOnInit() { this.counter$ = Observable.interval(1000).startWith(0); } }
  61. 61. Integrando com a Store @Component({…}) export class CounterComponent { counter$: Observable<number>; constructor(private store: Store<number>) {} ngOnInit() { this.counter$ = this.store.select<number>('counter'); } }
  62. 62. Integrando com a Store @Component({…}) export class CounterComponent { counter$: Observable<number>; constructor(private store: Store<number>) {} ngOnInit() { this.counter$ = this.store.select<number>('counter'); } } (1): Declarar Observable
  63. 63. Integrando com a Store @Component({…}) export class CounterComponent { counter$: Observable<number>; constructor(private store: Store<number>) {} ngOnInit() { this.counter$ = this.store.select<number>('counter'); } } (1): Declarar Observable (2): Injetar Store
  64. 64. Integrando com a Store @Component({…}) export class CounterComponent { counter$: Observable<number>; constructor(private store: Store<number>) {} ngOnInit() { this.counter$ = this.store.select<number>('counter'); } } (3): Obter estado relacionado ao Component
  65. 65. Template <div class="content"> <button (click)="increment()">+</button> <button (click)="decrement()">-</button> <button (click)="reset()">Reset Counter</button> <h3>{{counter$ | async}}</h3> </div>
  66. 66. Template <div class="content"> <button (click)="increment()">+</button> <button (click)="decrement()">-</button> <button (click)="reset()">Reset Counter</button> <h3>{{counter$ | async}}</h3> </div> Observable<number>
  67. 67. Eventos === Actions increment() { this.store.dispatch({ type: INCREMENT }); } decrement() { this.store.dispatch({ type: DECREMENT }); } reset() { this.store.dispatch({ type: RESET }); }
  68. 68. CRUD com Ajax
  69. 69. Definir ações export enum TaskActions { LOAD = '[Task] LOAD Requested', LOAD_SUCCESS = '[Task] LOAD Success', CREATE = '[Task] CREATE Requested', CREATE_SUCCESS = '[Task] CREATE Success', UPDATE = '[Task] UPDATE Requested', UPDATE_SUCCESS = '[Task] UPDATE Success', REMOVE = '[Task] REMOVE Requested', REMOVE_SUCCESS = '[Task] REMOVE Success', ERROR = '[Task] Error' }
  70. 70. Definir ações
  71. 71. Definir ações export class LoadAction extends NgRxAction<any> { type = TaskActions.LOAD; } export class LoadSuccessAction extends NgRxAction<Task[]> { type = TaskActions.LOAD_SUCCESS; }
  72. 72. No componente
  73. 73. No componente Evento de pedido para carregar a informação do servidor
  74. 74. No componente Evento de pedido para carregar a informação do servidor
  75. 75. Reducer
  76. 76. Reducer NÃO mudar o estado diretamente Estado deve ser imutável
  77. 77. Reducer NÃO mudar o estado diretamente Estado deve ser imutável Reducer DEVE ser uma função PURA Programação Funcional
  78. 78. Components: Dumb Components <li class="collection-item"> <span [class.task-completed]="task.completed">{{ task.title }}</span> <a class="secondary-content btn-floating" (click)="onRemove()"> <i class="material-icons circle">delete</i> </a> <a class="secondary-content btn-floating" (click)="onComplete()"> <i class="material-icons circle">done</i> </a> </li>
  79. 79. Components: Dumb Components @Component({}) export class TaskItemComponent { @Input() task: Task; @Output() remove: EventEmitter<any> = new EventEmitter(false); @Output() complete: EventEmitter<any> = new EventEmitter(false); onRemove() { this.remove.emit(); } onComplete() { this.complete.emit({ completed: !this.task.completed }); } }
  80. 80. Containers: Smart Components <div class="row"> <div class="col s6 offset-s3 input-field"> <app-task-form (createTask)=“onCreateTask($event)"> </app-task-form> <app-tasks-list [tasks]="tasks$" (remove)="onRemoveTask($event)" (complete)="onUpdateTask($event)" > </app-tasks-list> </div> </div> Escutam os eventos dos Componentes filhos e fazem o dispatch
  81. 81. Containers: Smart Components <div class="row"> <div class="col s6 offset-s3 input-field"> <app-task-form (createTask)=“onCreateTask($event)"> </app-task-form> <app-tasks-list [tasks]="tasks$" (remove)="onRemoveTask($event)" (complete)="onUpdateTask($event)" > </app-tasks-list> </div> </div> Escutam os eventos dos Componentes filhos e fazem o dispatch
  82. 82. Mas e a comunicação com servidor?
  83. 83. Redux apenas se interessa pela estado do cliente (frontend)
  84. 84. Store side effects Efeitos Colaterais @ngrx/effects
  85. 85. Effects Escuta a ação de Pedido e faz dispatch da ação de “Completo" - que atualiza o estado @Effect() loadAction$ = this.actions$ .ofType<task.LoadAction>(task.TaskActions.LOAD) .map(action => action.payload) .switchMap(payload => this.api .load() .map(res => new task.LoadSuccessAction(res)) .catch(error => this.handleError(error)) ); 😍
  86. 86. Service API o Serviço da API não sabe do estado nem do redux @Injectable() export class TaskService { private readonly API_TASKS_URL = `http://localhost:3001/tasks`; constructor(private http: HttpClient) {} load() { return this.http.get<Task[]>(this.API_TASKS_URL); } create(record: Task) { return this.http.post<Task>(this.API_TASKS_URL, record); } update(record: Task) { return this.http.put<Task>(`${this.API_TASKS_URL}/${record.id}`, record); } remove(id: string) { return this.http.delete<Task>(`${this.API_TASKS_URL}/${id}`); } }
  87. 87. Lazy Loading
  88. 88. @ngrx/platform (v4) @NgModule({ imports: [ StoreModule.forFeature('task', taskReducer), EffectsModule.forFeature([TaskEffects]) ], exports: [StoreModule, EffectsModule], providers: [TaskStoreService] }) export class TaskStoreModule {} @NgModule({ imports: [ StoreModule.forRoot(reducers), EffectsModule.forRoot([]), StoreRouterConnectingModule, !environment.production ? StoreDevtoolsModule.instrument({ maxAge: 50 }) : [] ], exports: [StoreModule] }) export class AppStoreModule { }
  89. 89. @ngrx/platform (v4) @NgModule({ imports: [ StoreModule.forFeature('task', taskReducer), EffectsModule.forFeature([TaskEffects]) ], exports: [StoreModule, EffectsModule], providers: [TaskStoreService] }) export class TaskStoreModule {} @NgModule({ imports: [ StoreModule.forRoot(reducers), EffectsModule.forRoot([]), StoreRouterConnectingModule, !environment.production ? StoreDevtoolsModule.instrument({ maxAge: 50 }) : [] ], exports: [StoreModule] }) export class AppStoreModule { }
  90. 90. Redux DevTools
  91. 91. Organização do Projeto (opinião da Loiane)
  92. 92. Módulo Main: App Store
  93. 93. Módulo Main: App Store Módulo Feature: Feature Store
  94. 94. Boas Práticas Estado
  95. 95. Evite Arrays!
  96. 96. Evite Arrays! Prefira Dicionários (Object Literals)
  97. 97. RxJS CombineLatest getCurrentTaskSelected() { return Observable.combineLatest( this.getTasks(), this.store.select(this.selectCurrentTaskId), (tasks, selectedId) => selectedId.map(id => tasks[id]) ); }
  98. 98. NgRx v5.x @ngrx/entity
  99. 99. @ngrx/entity import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; import { Task } from './../model/task'; export interface TaskState extends EntityState<Task> { isLoading: boolean; selectedTaskId: any; error: any; } export const taskAdapter: EntityAdapter<Task> = createEntityAdapter<Task>({ selectId: (task: Task) => task.id, sortComparer: false, }); export const taskInitialState: TaskState = taskAdapter.getInitialState({ isLoading: true, selectedTaskId: null, error: null });
  100. 100. @ngrx/entity import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; import { Task } from './../model/task'; export interface TaskState extends EntityState<Task> { isLoading: boolean; selectedTaskId: any; error: any; } export const taskAdapter: EntityAdapter<Task> = createEntityAdapter<Task>({ selectId: (task: Task) => task.id, sortComparer: false, }); export const taskInitialState: TaskState = taskAdapter.getInitialState({ isLoading: true, selectedTaskId: null, error: null });
  101. 101. @ngrx/entity import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; import { Task } from './../model/task'; export interface TaskState extends EntityState<Task> { isLoading: boolean; selectedTaskId: any; error: any; } export const taskAdapter: EntityAdapter<Task> = createEntityAdapter<Task>({ selectId: (task: Task) => task.id, sortComparer: false, }); export const taskInitialState: TaskState = taskAdapter.getInitialState({ isLoading: true, selectedTaskId: null, error: null });
  102. 102. @ngrx/entity export interface EntityState<T> { ids: string[]; entities: Dictionary<T>; } export interface EntityStateAdapter<T> { addOne<S extends EntityState<T>>(entity: T, state: S): S; addMany<S extends EntityState<T>>(entities: T[], state: S): S; addAll<S extends EntityState<T>>(entities: T[], state: S): S; removeOne<S extends EntityState<T>>(key: string, state: S): S; removeMany<S extends EntityState<T>>(keys: string[], state: S): S; removeAll<S extends EntityState<T>>(state: S): S; updateOne<S extends EntityState<T>>(update: Update<T>, state: S): S; updateMany<S extends EntityState<T>>(updates: Update<T>[], state: S): S; } export declare type EntitySelectors<T, V> = { selectIds: (state: V) => string[]; selectEntities: (state: V) => Dictionary<T>; selectAll: (state: V) => T[]; selectTotal: (state: V) => number; };
  103. 103. Reducer case TaskActions.LOAD_SUCCESS: { return ...taskAdapter.addMany(action.payload, state); } case TaskActions.CREATE_SUCCESS: { return …taskAdapter.addOne(action.payload, state); } case TaskActions.UPDATE_SUCCESS: { return ...taskAdapter.updateOne( { id: action.payload.id, changes: action.payload }, state ); } case TaskActions.REMOVE_SUCCESS: { return { ...taskAdapter.removeOne(action.payload.id, state), error: null }; } 👏 👏 👏
  104. 104. VSCode
  105. 105. Em breve!
  106. 106. Prós e Contras: 1.Fluxo unidirecional ✅ 2.Debug volta ao tempo (DevTools) ✅ 3.Separação do código ✅ 4.Fácil debug e bug fix (1, 2, e 3) ✅ 5.Mais fácil pra testar devido à funções puras ✅ 6.Melhor performance (onPush) ✅ 7.Serialização do estado ✅ 8.Mais uma camada == mais código ⛔
  107. 107. https://github.com/loiane/angular-ngrx4-example
  108. 108. Pra estudar mais… • https://angular.io/docs/ts/latest/guide/reactive-forms.html • https://angular.io/docs/ts/latest/guide/server-communication.html • https://angular.io/docs/ts/latest/guide/router.html • https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/categories.md • https://gist.github.com/btroncone/d6cf141d6f2c00dc6b35 • http://rxmarbles.com/ • http://reactivex.io/documentation/operators • https://github.com/ngrx • https://github.com/ngrx/example-app • https://auth0.com/blog/understanding-angular-2-change-detection/ • http://blog.brecht.io/A-scalable-angular2-architecture/ • http://blog.mgechev.com/2016/04/10/scalable-javascript-single-page-app-angular2- application-architecture/
  109. 109. Obrigada!
  110. 110. @loiane github.com/loiane loiane.com loiane.training youtube.com/loianegroner

×