Селектор магазина Ngrx 8.0.1 не обновляется после изменения состояния (я возвращаю новый объект состояния) - PullRequest
0 голосов
/ 06 июля 2019

Я управляю состоянием моего приложения с помощью Ngrx. Я создал эффект, который срабатывает, когда отправляется действие «[Todo] Query». Эффект вызывает коллекцию «Todos» из firestore через TodosService.getTodos () и отправляет действие «[Todo] Load success» с полезной нагрузкой todos, когда оно успешно, или запускает действие «[Todo] Load Error» с ошибка при возникновении ошибки.

В Todo.component я назначаю селектор "getTodos" переменной "todos $" и подписываюсь через асинхронный канал в шаблоне. Но изменения состояния не отражаются, когда редуктор УСПЕХА возвращает новое состояние со списком задач.

Я использую расширение Chrome "Ngrx Store Devtools", и оно показывает мне изменения статуса, в том числе, когда список "Todos" из пустого превращается в несколько задач.

это состояние приложения до того, как "Query" отправит эффект "Success" со списком задач: enter image description here

это состояние приложения после того, как редуктор "success" вернул новое состояние со списком задач: enter image description here

Что привлекает мое внимание, так это то, что инструмент обновляет изменение состояния, но селектор этого не делает

Это весь код, который задействован. Может быть, где-то есть ошибка.

todos.actions.ts:

import { createAction, props } from '@ngrx/store';
import { Todo } from '../../models/domain/todo';
import { TodoId } from '../../models/domain/todo-id/todo-id.model';

export const CREATE = createAction('[Todo] Add Todo', props<{ todo: Todo }>());
export const QUERY = createAction('[Todo] Query');
export const SUCCESS = createAction('[Todo] Load Todos Success', props<{ todos: TodoId[] }>());
export const ERROR = createAction('[Todo] Load Todos Error', props<{error: any}>());

todos.reducer.ts:

import { Todo } from '../../models/domain/todo';
import * as actions from '../actions/todo.actions';
// import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { createReducer, on, Action } from '@ngrx/store';

// export const todoAdapter = createEntityAdapter<Todo>();

export interface State {
  todos: Todo[];
  loadedSuccess: boolean;
  loadError: any;
}


export const initialState: State = {
  todos: [],
  loadedSuccess: false,
  loadError: false
};

const todoReducerFn = createReducer(
  initialState,
  on(actions.QUERY, (state) => state),
  on(actions.SUCCESS, handleSuccess),
  on(actions.ERROR, (state, action) => ({ ...state, loadError: action.error }))
);


export function reducer(state: State | undefined, action: Action): State {
  return todoReducerFn(state, action);
}

function handleSuccess(state: State, action): State {
  return Object.assign(
    {},
    state,
    {
      todos: action.todos.slice(),
      loadedSuccess: true
    });
}

export const getTodos = (state: State) => state.todos;
export const getSuccessTodos = (state: State) => state.loadedSuccess;
export const getLoadError = (state: State) => state.loadError;

todos.effects.ts:

import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Observable, EMPTY, of } from 'rxjs';
import { Action } from '@ngrx/store';
import * as actions from '../actions/todo.actions';
import { mergeMap, map, catchError, retry, switchMap } from 'rxjs/operators';
import { TodosService } from '../../services/todos-service/todos.service';
@Injectable()
export class TodosEffects {
  constructor(
    private actions$: Actions,
    private todosService: TodosService
  ) { }

  query$: Observable<Action> = createEffect(() => this.actions$.pipe(
    ofType(actions.QUERY),
    switchMap(() =>
      this.todosService._getAll().pipe(
        map((todos) => actions.SUCCESS({ todos: todos })),
        catchError((error) => of(actions.ERROR({ error: error })))
      )
    )
  ));
}

todos.service.ts:

import { Injectable } from '@angular/core';
import { Md5 } from 'ts-md5';
import { Todo } from '../../models/domain/todo';
import * as fromStore from '../../store/reducers/index';
import * as todoActions from '../../store/actions/todo.actions';
import { Store } from '@ngrx/store';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { map } from 'rxjs/operators';
import { TodoId } from '../../models/domain/todo-id/todo-id.model';

@Injectable({
  providedIn: 'root'
})
export class TodosService {
  todosCollection: AngularFirestoreCollection<Todo>;

  constructor(
    private store: Store<fromStore.State>,
    private readonly db: AngularFirestore
  ) {
  }

  _getAll() {
    return this.db.collection<Todo>('todos').stateChanges().pipe(
      map(actions =>
        actions.map(a => {
          const data = a.payload.doc.data() as Todo;
          const id = a.payload.doc.id;
          return { id, ...data };
        })
      )
    );
  }

  addTodo(text: string): void {
    if (text != '') {
      let newTodo = this.newTodo(text);
      const id = this.db.createId();
      this.todosCollection.doc<Todo>(id).set({ ...newTodo });
      this.store.dispatch(todoActions.CREATE({ todo: newTodo }));
    }
  }
}

todos.component.ts:

import { Component, OnInit } from '@angular/core';
import { Todo } from '../../models/domain/todo';
import { TodosService } from '../../services/todos-service/todos.service';
import { Subscription, Observable } from 'rxjs';
import { Store, select } from '@ngrx/store';
import * as fromStore from '../../store/reducers/index';
import * as todoActions from '../../store/actions/todo.actions';
import { TodoId } from 'src/app/models/domain/todo-id/todo-id.model';
import { tap } from 'rxjs/operators';

@Component({
  selector: 'app-todos',
  templateUrl: './todos.component.html',
  styleUrls: ['./todos.component.scss']
})
export class TodosComponent implements OnInit {
  todos: Todo[];
  todosSubscription: Subscription;
  todos$: Observable<Todo[]> = this.store.select(fromStore.getTodos).pipe(
    tap((todos) => console.log(todos))
  );
  loadedSucess$: Observable<boolean>;

  constructor(
    private todosService: TodosService,
    private store: Store<fromStore.State>
  ) { }

  ngOnInit() {
    this.store.dispatch(todoActions.QUERY());
  }

}

todos.component.html:


    <ng-container *ngFor="let todo of todos$ | async">
      <mat-list-item *ngIf="!todo.completed">
        <div fxLayout="row" fxLayoutAlign="space-around center" fxLayoutGap="20px">
          <mat-checkbox (click)="completeTodo(todo)">
             {{ todo.text }}
          </mat-checkbox>
          <button role="button"
                  type="button" 
                  mat-icon-button 
                  (click)="onClickRemoveTodo(todo)"
                  aria-label="Remover tarea de la lista">
                  <mat-icon>clear</mat-icon>
          </button>
        </div>
     </mat-list-item>
    </ng-container> 

app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ServiceWorkerModule } from '@angular/service-worker';
import { TodosModule } from './todos/todos.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SharedModule } from './shared/shared.module';
import { MaterialModule } from './shared/material/material.module';
import { FlexLayoutModule } from "@angular/flex-layout";
import { NavigationModule } from './navigation/navigation.module';
import { ReactiveFormsModule } from '@angular/forms';
import { AngularFireModule } from '@angular/fire';
import { AngularFireAuthModule } from '@angular/fire/auth';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { AppEffects } from '../store/effects/app.effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../../environments/environment';
import { AuthEffects } from '../store/effects/auth.effects';
import { HttpClientModule } from '@angular/common/http';
import { reducers, metaReducers } from '../store/reducers';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireAuthModule,
    AngularFirestoreModule,
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
    BrowserAnimationsModule,
    SharedModule,
    MaterialModule,
    FlexLayoutModule,
    NavigationModule,
    ReactiveFormsModule,
    StoreDevtoolsModule.instrument({
      maxAge: 25
    }),
    StoreModule.forRoot(reducers, { metaReducers }),
    TodosModule,
    EffectsModule.forRoot([AppEffects, AuthEffects]),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

todos.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { TodosRoutingModule } from './todos-routing.module';
import { TodosComponent } from './todos.component';
import { MaterialModule } from '../shared/material/material.module';
import { FlexLayoutModule } from '@angular/flex-layout';
import * as fromTodos from '../../store/reducers/todos.reducer';
import { StoreModule } from '@ngrx/store';
import { TodosEffects } from '../../store/effects/todos.effects';
import { EffectsModule } from '@ngrx/effects';
@NgModule({
  declarations: [
    TodosComponent
  ],
  imports: [
    CommonModule,
    TodosRoutingModule,
    MaterialModule,
    FlexLayoutModule,
    StoreModule.forFeature('todos', fromTodos.reducer),
    EffectsModule.forFeature([TodosEffects])
  ]
})
export class TodosModule { }

магазин / редукторы / index.ts

import {
  ActionReducerMap,
  createFeatureSelector,
  createSelector,
  MetaReducer,
  ActionReducer
} from '@ngrx/store';
import { environment } from '../../../environments/environment';
import * as fromAuth from './auth.reducer';
import * as fromTodos from './todos.reducer';

export interface State {
  auth: fromAuth.State;
  todos: fromTodos.State;
}

export const reducers: ActionReducerMap<State> = {

  auth: fromAuth.reducer,
  todos: fromTodos.reducer,
};


export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
  return (state, action) => {
    console.log('state', state);
    console.log('action', action);

    return reducer(state, action);
  };
}

export const metaReducers: MetaReducer<State>[] = !environment.production ? [debug] : [];


export const selectAuthState = createFeatureSelector<fromAuth.State>('auth');
export const getUserName = createSelector(selectAuthState, fromAuth.getUserName);
export const getFriendlyName = createSelector(selectAuthState, fromAuth.getFriendlyName);

export const selectTodoState = createFeatureSelector<fromTodos.State>('todos');
export const getTodos = createSelector(selectTodoState, fromTodos.getTodos);
export const getSuccessTodos = createSelector(selectTodoState, fromTodos.getSuccessTodos);
export const getLoadError = createSelector(selectTodoState, fromTodos.getLoadError);

Это версии зависимостей:

    "@angular/animations": "~7.2.0",
    "@angular/cdk": "~7.3.7",
    "@angular/common": "~7.2.0",
    "@angular/compiler": "~7.2.0",
    "@angular/core": "~7.2.0",
    "@angular/fire": "^5.2.1",
    "@angular/flex-layout": "^7.0.0-beta.24",
    "@angular/forms": "~7.2.0",
    "@angular/material": "^7.3.7",
    "@angular/platform-browser": "~7.2.0",
    "@angular/platform-browser-dynamic": "~7.2.0",
    "@angular/router": "~7.2.0",
    "@angular/service-worker": "~7.2.0",
    "@ngrx/effects": "^8.0.1",
    "@ngrx/entity": "^8.0.1",
    "@ngrx/router-store": "^8.0.1",
    "@ngrx/store": "^8.0.1",
    "@ngrx/store-devtools": "^8.0.1",
    "core-js": "^2.5.4",
    "firebase": "^6.2.4",
    "hammerjs": "^2.0.8",
    "rxjs": "~6.3.3",
    "ts-md5": "^1.2.4",
    "tslib": "^1.9.0",
    "zone.js": "~0.8.26"
...