ngrx - update single item in a list of items

18.8k views Asked by At

I have somewhat the following Application State tree:

AppState
{
    MyPage1 - MyList - MyItem[]
    MyPage2 - MySubPage - MyList - MyItem[]
    MyPage3 - MyItem   //can be stand-alone as well
}

So, I have a list of items on a page. I load this list by dispatching an action to the MyList reducer. Once items are loaded, they are passed as an input to the MyListComponent, which accordingly creates MyItemComponents with the corresponding inputs. MyItemComponent can be stand-alone as well. So, it's not always dependent on the MyList.

The thing is that each of the MyItems has its own actions (e.g. can be liked or edited). And I'm currently stuck where and what action should I dispatch in that case. How do I identify which item was updated and which list should I update.

What would be the action flow when user updates one of the items in a list?

And also - would the state change of MyList be detected if I change one of the MyItem directly?

Scenario I might think of is (while having all appropriate actions on MyItem itself) to add extra actions to the MyList - e.g.:

updateItem(payload: 
    {
        updateType, //e.g. like, edit, delete
        data,       //data needed to make an update
        itemId      //index in the MyItem array
    })

Then, somehow I fire another action from MyList to the specified MyItem from the array, item fires appropriate action to update its own state (updated via @Effect) If an update was successful, a new action is fired to update the list.

To be honest, it all seems pretty verbose and vague. Not even sure how it would be implemented in practice. Really need suggestion from someone with experience.

p.s. I should mention that all the data is fetched and sent back from/to the REST api.

Edit: If following the approach from Comprehensive Introduction to @ngrx/store, the app would have tones of duplicate actions (each container - MyList - would have all the actions from MyItem repeated). Would there be a more suitable approach for my case?

1

There are 1 answers

5
Leon Radley On BEST ANSWER

The way to do it is to have a sub reducer which handles a sub part of the state tree.

In the case where you need to send actions to a single item in the array you pass along all of those actions to the sub reducer that handles a single item.

Here's an example of a news item, that has comments. I hope it clears things up a bit.

import { Action } from '@ngrx/store';
import { AppState } from '../reducer';
import { NewsItem } from './news.model';
import * as newsActions from './news.actions';

export interface NewsState {
  readonly loading: boolean;
  readonly entities: NewsItem[];
}

export interface AppStateWithNews extends AppState {
  readonly news: NewsState;
}

const initialState: NewsState = {
  loading: false,
  entities: [],
};

const newsItemReducer = (newsItem: NewsItem, action: newsActions.NewsActionTypes): NewsItem => {

  switch (action.type) {
    case newsActions.Types.UPDATE:
    case newsActions.Types.REMOVE:
    case newsActions.Types.COMMENT_CREATE:
    case newsActions.Types.COMMENT_REMOVE:
      return Object.assign({}, newsItem, {
        action: true
      });

    case newsActions.Types.UPDATE_SUCCESS:
      return Object.assign({}, action.payload, {
        action: false
      });

    case newsActions.Types.COMMENT_CREATE_SUCCESS:
      return Object.assign({}, newsItem, {
        action: false,
        comments: [action.payload.comment, ...newsItem.comments]
      });

    case newsActions.Types.COMMENT_REMOVE_SUCCESS:
      return Object.assign({}, newsItem, {
        action: false,
        comments: newsItem.comments.filter(i => i.id !== action.payload.commentId)
      });

    default:
      return newsItem;
  }
};

export const newsReducer = (state: NewsState = initialState, action: newsActions.NewsActionTypes): NewsState => {

  switch (action.type) {
    case newsActions.Types.LIST:
      return Object.assign({}, state, { loading: true });

    case newsActions.Types.LIST_SUCCESS:
      return {
        loading: false,
        entities: action.payload
      };

    case newsActions.Types.CREATE_SUCCESS:
      return Object.assign({
        loading: false,
        entities: [action.payload, ...state.entities]
      });

    case newsActions.Types.REMOVE_SUCCESS:
      return Object.assign({
        loading: false,
        entities: state.entities.filter(newsItem => newsItem !== action.payload)
      });

    // Delegate to newsItemReducer
    case newsActions.Types.UPDATE:
    case newsActions.Types.UPDATE_SUCCESS:
    case newsActions.Types.COMMENT_CREATE:
    case newsActions.Types.COMMENT_CREATE_SUCCESS:
    case newsActions.Types.COMMENT_REMOVE:
    case newsActions.Types.COMMENT_REMOVE_SUCCESS:
      return Object.assign({}, state, {
        entities: state.entities.map(newsItem => {
          // Only call sub reducer if the incoming actions id matches
          if (newsItem.id === (action.payload.newsItem || action.payload).id) {
            return newsItemReducer(newsItem, action);
          }
          return newsItem;
        })
      });

    default:
      return state;
  }

};

And to see how to call these here is the news.actions.

import { Action } from '@ngrx/Store';
import { NewsItem } from './news.model';
import { Response } from '@angular/http';

export const Types = {
  LIST: '[News] List',
  LIST_SUCCESS: '[News] List Success',
  LIST_ERROR: '[News] List ERROR',

  CREATE: '[News] Create',
  CREATE_SUCCESS: '[News] Create Success',
  CREATE_ERROR: '[News] Create Error',

  UPDATE: '[News] Update',
  UPDATE_SUCCESS: '[News] Update Success',
  UPDATE_ERROR: '[News] Update Error',

  REMOVE: '[News] Remove',
  REMOVE_SUCCESS: '[News] Remove Success',
  REMOVE_ERROR: '[News] Remove Error',

  COMMENT_CREATE: '[News] Comment Create',
  COMMENT_CREATE_SUCCESS: '[News] Comment Create Success',
  COMMENT_CREATE_ERROR: '[News] Comment Create Error',

  COMMENT_REMOVE: '[News] Comment Remove',
  COMMENT_REMOVE_SUCCESS: '[News] Comment Remove Success',
  COMMENT_REMOVE_ERROR: '[News] Comment Remove Error',
};

/**
 * List
 */
export class List implements Action {
  type = Types.LIST;
  constructor(public payload?: any) {}
}
export class ListSuccess implements Action {
  type = Types.LIST_SUCCESS;
  constructor(public payload: NewsItem[]) {}
}
export class ListError implements Action {
  type = Types.LIST_ERROR;
  constructor(public payload: Response) {}
}

/**
 * Create
 */
export class Create implements Action {
  type = Types.CREATE;
  constructor(public payload: NewsItem) {}
}
export class CreateSuccess implements Action {
  type = Types.CREATE_SUCCESS;
  constructor(public payload: NewsItem) {}
}
export class CreateError implements Action {
  type = Types.CREATE_ERROR;
  constructor(public payload: Response) {}
}

/**
 * Update
 */
export class Update implements Action {
  type = Types.UPDATE;
  constructor(public payload: NewsItem) {}
}
export class UpdateSuccess implements Action {
  type = Types.UPDATE_SUCCESS;
  constructor(public payload: NewsItem) {}
}
export class UpdateError implements Action {
  type = Types.UPDATE_ERROR;
  constructor(public payload: Response) {}
}

/**
 * Remove
 */
export class Remove implements Action {
  type = Types.REMOVE;
  constructor(public payload: NewsItem) {}
}
export class RemoveSuccess implements Action {
  type = Types.REMOVE_SUCCESS;
  constructor(public payload: NewsItem) {}
}
export class RemoveError implements Action {
  type = Types.REMOVE_ERROR;
  constructor(public payload: Response) {}
}

/**
 * Create Comment
 */
export class CommentCreate implements Action {
  type = Types.COMMENT_CREATE;
  payload: {
    newsItem: NewsItem,
    comment: string
  };

  constructor(newsItem: NewsItem, comment: string) {
    this.payload = {
      newsItem,
      comment
    };
  }
}
export class CommentCreateSuccess implements Action {
  type = Types.COMMENT_CREATE_SUCCESS;
  payload: {
    newsItem: NewsItem,
    comment: string
  };
  constructor(newsItem: NewsItem, comment: string) {
    this.payload = {
      newsItem,
      comment
    };
  }
}
export class CommentCreateError implements Action {
  type = Types.COMMENT_CREATE_ERROR;
  constructor(public payload: Response) {}
}

/**
 * Remove Comment
 */
export class CommentRemove implements Action {
  type = Types.COMMENT_REMOVE;
  payload: {
    newsItem: NewsItem,
    commentId: string
  };

  constructor(newsItem: NewsItem, commentId: string) {
    this.payload = {
      newsItem,
      commentId
    };
  }
}
export class CommentRemoveSuccess implements Action {
  type = Types.COMMENT_REMOVE_SUCCESS;
  payload: {
    newsItem: NewsItem,
    commentId: string
  };
  constructor(newsItem: NewsItem, commentId: string) {
    this.payload = {
      newsItem,
      commentId
    };
  }
}
export class CommentRemoveError implements Action {
  type = Types.COMMENT_REMOVE_ERROR;
  constructor(public payload: Response) {}
}

export type NewsActionTypes =
    List
  | ListSuccess
  | ListError
  | Create
  | CreateSuccess
  | CreateError
  | Update
  | UpdateSuccess
  | UpdateError
  | Remove
  | RemoveSuccess
  | RemoveError
  | CommentCreate
  | CommentCreateSuccess
  | CommentCreateError
  | CommentRemove
  | CommentRemoveSuccess
  | CommentRemoveError;