Mar
Mar

Reputation: 1606

Update the state after a server call?

I am confused with updating and the returning the right state from the reducer after receiving a new set of data from the server. I actually don't understand what to return(how to update the state) after server response. To be able to more clear about my confusion:

For an example: On a todo app I would have a reducer:

// imports..

export function todoReducer(state = [], action) {
    switch (action.type) {
        // some cases...

        case todoActions.TODO_LOADED: {
            return [
                ...state,
                {
                ...action.payload   
                } 
            ]    
        }

        // some more cases...
        default: 
           return state; 
    }
}

and the effects:

@Effect() todoList$: Observable<any> = this.action$
        .ofType(todoActions.TODO_LOAD)
        .switchMap(() => {
            return this.http
            .get('/rest/todo-list')
            .map((todos: Todos) => new todoActions.TodoLoaded(todos))
            .catch(err => Observable.of(new todoActions.TodoFailed(err)));
        });

When the todoActions.TODO_LOAD is triggered, I will obviously get the todo list. Until here it is clear.

However, How do I update the state in my reducer if a user removes a todo item from UI? I would dispatch a todoActions.TodoRemove action from my component with the id of a todo to remove and...

For exp:

@Effect() removeTodo$: Observable<any> = this.action$
        .ofType(todoActions.TODO_REMOVE)
        .switchMap((action : todoActions.TodoRemove) => { 
            return this.http
            .delete(`/rest/todo-list/${action.payload['todoId']}`)
            .map(() =>  {
                // What kind of action should I apply here?
            })
            .catch(err => Observable.of(new todoActions.TodoFailed(err)));
        });

So the question is what kind of action should I trigger after a successful response from the server(let's say I get only 200)?

If I would trigger todoActions.TODO_LOAD again. It would merge the list with the previous state and I would get the todos double...

But we shouldn't mutate the state... So I shouldn't refresh the state on each time new todos loaded... right?

or do I just have to create a new action like todoActions.TODO_REMOVE_SUCCESSED and return the new todo list from server without the state... like:

case todoActions.TODO_REMOVE_SUCCESSED: {  
    return [...action.payload ]
}

Would this be anti-pattern?

Maybe this is a stupid question but I don't get the idea here...

Thanks in advance!

Upvotes: 1

Views: 1989

Answers (1)

AndreaM16
AndreaM16

Reputation: 3985

I would give a shot a more standard, clean and easy solution. Also going to solve your duplicates issue and always updating your store properly.

You need 6 actions:

import { Action } from '@ngrx/store';

/* App Models */
import { Todo } from './todo.model';

export const TODO_GET = '[Todo] get todo';
export const TODO_GET_SUCCESS = '[Todo] get todo success';
export const TODO_DELETE = '[Todo] delete todo';
export const TODO_DELETE_SUCCESS = '[Todo] delete todo success';
export const TODO_GET_BY_ID = '[Todo] get todo by id';
export const TODO_GET_BY_ID_SUCCESS = '[Todo] get todo by id success';

// Gets Todos from APIs
export class TodoGetAction implements Action {
  readonly type = TODO_GET;
}

// Returns APIs fetched Todos
export class TodoGetSuccessAction implements Action {
  readonly type = TODO_GET_SUCCESS;
  constructor(public payload: Todo[]) {}
}

// Deletes a Todo given its string id
export class TodoDeleteAction implements Action {
  readonly type = TODO_DELETE;
  constructor(public payload: string) {}
}

// True -> Success, False -> Error
export class TodoDeleteSuccessAction implements Action {
  readonly type = TODO_DELETE_SUCCESS;
  constructor(public payload: Todo[]) {}
}

// Takes the id of the todo
export class TodoGetByIdAction implements Action {
  readonly type = TODO_GET_BY_ID;
  constructor(public payload: string) {}
}

// Returns todo by id
export class TodoGetByIdSuccessAction implements Action {
  readonly type = TODO_GET_BY_ID_SUccess;
  constructor(public payload: Todo) {}
}

export type All =
  | TodoGetAction
  | TodoGetSuccessAction
  | TodoDeleteAction
  | TodoDeleteSuccessAction
  | TodoGetByIdAction
  | TodoGetByIdSuccessAction;

Then you'll have a reducer with a simple state which has an array of todos and a current todo selected by id. We handle all the successful actions here while we handle all the normal ones in our effects:

import { createFeatureSelector } from '@ngrx/store';
import { createSelector } from '@ngrx/store';

/* ngrx */
import * as TodoActions from './todo.actions';

/* App Models */
import { Todo } from './todo.model';

// Get all actions
export type Action = TodoActions.All;

export interface TodoState {
  todos: Todo[];
  todoById: Todo;
}

// Initial state with empty todos array
export const todoInitialState: TodoState = {
   todos: [],
   todoById: new Todo({})
}

/* Selectors */
export const selectTodoState = createFeatureSelector<
  TodoState
>('todo');
// Select all Todos
export const selectTodos = createSelector(
  selectTodoState,
  (state: TodoState) => state.todos
);
export const selectTodoByID = createSelector(
  selectTodoState,
  (state: TodoState) => state.todoById
);

export function todoReducer(
  state: TodoState = todoInitialState,
  action: Action
) {
  switch (action.type) {
    case TodoActions.TODO_GET_SUCCESS:
      const oldTodos = state.todos;
      // Add new todos to old ones
      const newTodos = oldTodos.concat(action.payload);
      // Cast to set to have only unique todos
      const uniqueTodos = new Set(newTodos);
      // Cast back to array to get an array out of the set
      const finalTodos = Array.from(uniqueTodos);
      return {
         ...state,
         todos: [...finalTodos]
      }
    case TodoActions.TODO_DELETE_SUCCESS:
       return {
         ...state,
         todos: state.todos.filter( todo => return todo.id !== action.payload)
       }
     case TodoActions.TODO_GET_BY_ID_SUCCESS:
       return {
         ...state,
         todoById: state.todos.filter( todo => return todo.id === action.payload)[0]
       }
     default: 
       return state; 
  }
}

Then you'll have 3 effects:

import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { Store } from '@ngrx/store';

/** rxjs **/
import { mergeMap } from 'rxjs/operators/mergeMap';
import { catchError } from 'rxjs/operators/catchError';
import { map } from 'rxjs/operators/map';
import { of } from 'rxjs/observable/of';

/** ngrx **/
import * as TodoActions from './todo.actions';
import { AppState } from '../app-state.interface';

/** App Services **/
import { TodoService } from './province.service';

@Injectable()
export class TodoEffects {

  @Effect()
  getTodos$ = this.actions$.ofType(TodoActions.TODO_GET).pipe(
    mergeMap(() => {
      // I Imagine you can move your api call in such service
      return this.todoService.getTodos().pipe(
        map((todos: Todo[]) => {
          return new TodoActions.TodoGetSuccessAction(todos);
        }),
        catchError((error: Error) => {
          return of(// Handle Error Here);
        })
      );
    })
  );

  @Effect()
  deleteTodo$ = this.actions$.ofType(TodoActions.TODO_DELETE).pipe(
    mergeMap((action) => {
        return new TodoActions.TodoDeleteSuccessAction(action.payload);
    })
  );

  @Effect()
  getTodoByID$ = this.actions$.ofType(TodoActions.TODO_GET_BY_ID).pipe(
    mergeMap((action) => {
        return new TodoActions.TodoGetByIdSuccessAction(action.payload);
    })
  );

  constructor(
    private todoService: TodoService,
    private actions$: Actions,
    private store: Store<AppState>
  ) {}
}

And finally in your Todo Component you can dispatch and subscribe to changes in order to have your fresh info out of the store. Of course, I'm assuming you have some methods to trigger a research by id dispatching a new GetTodoById passing todo's id to it and also other methods to hanle the elimination in the same way. Every modification you'll make on the store will be reflected to the entries in your components that are subscribed to them via selectors.

. . .

todos$: Observable<Todo[]>;
todoById$: Observable<Todo>;

constructor(private store: Store<AppState>) {
    this.todos$ = this.store.select(selectTodos);
    this.todo$ = this.store.select(selectTodoById);
}

ngOnInit() {
    this.store.dispatch(new TodoActions.TodoGetAction);
}

Upvotes: 1

Related Questions