Home > Software design >  ngrx store - select one entity to show in a separated template
ngrx store - select one entity to show in a separated template

Time:02-28

I have a blog application and I'm using ngrx.

the application -for now- have two objects in the store

{
"posts": [...],
"users": [...]
}

now I want to make a separated component to show a single post (selected-post.component).

I tried to select the entity by its id using a selector like this:

export const selectPostById = (id: string) => createSelector(
  selectEntities,
  entities => entities[id]
);

and then get it in the component:

postId: string = localStorage.getItem("_selectedPostId");

selectedPost: Observable<Post> = this.store.select(selectPostById(postId));

then to show it in the view I used:

<ng-container *ngIf="selectedPost | async">
  <div >
    <div >{{(selectedPost | async).title}}</div>
    <div >
      <img [src]="(selectedPost | async).imageSrc">
    </div>
    <div >
      {{(selectedPost | async).content}}
    </div>
    <div >
      <a  href="categories/category" *ngFor="let ct of (selectedPost | async).categories">
        {{ct}}
      </a>
    </div>
    <div >
      {{(selectedPost | async).date}}
    </div>
    <div >
      <a href="users/otheruser">{{(selectedPost | async).author.name}}</a>
    </div>
  </div>
  <div >Comments</div>
  <div >
    <div  *ngFor="let comment of (selectedPost | async).comments">
      <div >
        <a href="users/otheruser">{{comment.author.name}}</a>
      </div>
      <div >{{comment.content}}</div>
      <div >{{comment.date}}</div>
      <div  *ngIf="comment.replies">
        <div  *ngFor="let reply of comment.replies">
          <div >
            <a href="users/otheruser">{{reply.author.name}}</a>
          </div>
          <div >{{reply.content}}</div>
          <div >{{reply.date}}</div>
        </div>
      </div>
    </div>
  </div>
</ng-container>

So the problem is that the post is not shown with error in console:

ERROR TypeError: Cannot read properties of undefined (reading: '1')

I selected the post with the id "1" and it exists in the state shown by redux devtools.

Any ideas?.

EDIT: the reducer:

import { Action, createFeatureSelector, createReducer, createSelector, on } from '@ngrx/store';
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import * as postActions from './actions';
import { Post } from 'src/app/models/post';

export interface PostState extends EntityState<Post> {
  error: any;
  selectedPostId: string | null;
}

export function selectUserId(a: Post): string {
  return a.id;
}

export const adapter: EntityAdapter<Post> = createEntityAdapter<Post>({
  selectId: selectUserId,
  sortComparer: false
});

export const initialState = adapter.getInitialState({error: null,selectedPostId: null});

const testsReducer = createReducer(
  initialState,
  on(postActions.loadPostsSuccess, (state, {posts}) => adapter.setAll(posts, state)),
  on(postActions.loadPostsFailed, (state, {error}) => ({...state, error})),
  on(postActions.setSelectedPostId, (state, {id}) => ({...state, selectedPostId: id}))
);

export function reducer(state: PostState, action: Action) {
  return testsReducer(state, action);
}

// Selectors

const {
  selectIds,
  selectEntities,
  selectAll,
  selectTotal,
} = adapter.getSelectors();

const feature = createFeatureSelector<PostState>('posts');

export const getPosts = createSelector(feature,selectAll);

export const getSelectedPostId = (state: PostState) => state.selectedPostId;

export const selectPostId = createSelector(
  feature,
  getSelectedPostId
);

export const selectPostById = (id: string) => createSelector(
  selectEntities,
  entities => entities[id]
);

CodePudding user response:

You have to link the selectEntities selector from the adapter to feature one (created by createFeatureSelector function), then use it to get the required entity by Id.

You can try the following:

// Selectors
const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors();

const feature = createFeatureSelector<PostState>('posts');

// ...

// Create a selector from the feature selector and selectEntities one.
export const selectPostEntities = createSelector(feature, selectEntities);

// Use the new selector `selectPostEntities` to slice the post entity from it.
export const selectPostById = (id: number) =>
    createSelector(selectPostEntities, (entities) => entities[id]);

Besides, you can optimize your component template by subscribing to the selectedPost observable using asyn pipe once only, like the following:

<ng-container *ngIf="selectedPost | async as _selectedPost">
<!-- You can now use the `_selectedPost` variable without `async` pipe for all the elements inside the ng-container -->
</ng-container>
  • Related