So I've been going on about this for a while and I can't figure out what I'm doing wrong. I have a child component, which is a modal. The child component has a search bar, which when used emits to the parent, who in turns uses a ngrx store facade to query data. The parent subscribes to all facade observables on init. The parent then passes the data to the child component to render.
Expected behavior:
- Search in child, send query string to parent
- Parent receives emit and queries facade with the string
- Facade dispatches an action to the store, which runs an effect to get the new data.
- As parent is subscribed to facade observable, should receive updated data and pass it to child.
- Child renders new data.
Current behavior:
- Search in child, sends query string to parent (success)
- Parent receives emit and queries facade with the string (success)
- Facade dispatches an action to the store, which runs an effect to the new data (success).
- Parent receives data, but pipes it through the wrong observable first and then through the intended observable (failure).
- Child renders new data, but data behind modal is broken (failure).
Essentially on step 4, the emit from the child triggers both of the subscriptions in 1. below. The first one receives the string, and receives data which is from Device[] type, passes it to the list component, which breaks, while the second one received data from Device[] type as well and renders properly.
Posting snippets of the code:
- Parent
@Component({
selector: 'app-marketplace',
templateUrl: './marketplace.component.html',
styleUrls: ['./marketplace.component.sass'],
})
export class MarketplaceComponent {
public limit: number = 18;
public offset: number = 0;
public postings!: Posting[] | null;
public devices!: Device[] | null;
constructor(
public modal: ModalService,
private postingFacade: PostingFacade,
private userFacade: UserFacade
) {
this.postingFacade.postingData$.subscribe({
next: (data: Posting[] | null) => (this.postings = data),
error: (err: string | null) => console.log(err),
});
this.postingFacade.devicesData$.subscribe({
next: (data: Device[] | null) => (this.devices = data),
error: (err: string | null) => console.log(err),
});
}
searchPostings(query: string): void {
this.limit = 18;
this.postingFacade.queryPostings(query, this.limit, this.offset);
}
fetchDeviceList(query: string): void {
this.postingFacade.queryDevices(query, 10);
}
fetchDeviceDetails(key: string): void {
this.postingFacade.getDeviceDetails(key);
}
}
- Child
@Component({
selector: 'app-create',
templateUrl: './create.component.html',
styleUrls: ['./create.component.sass'],
})
export class CreateComponent implements AfterViewInit {
public display$!: Observable<boolean>;
@Input() user!: User | null;
@Input() devices!: Device[] | null;
@Input() deviceDetails!: Device | null;
@Output() requestDeviceList = new EventEmitter<string>();
@ViewChild('searchInput') searchDevice!: ElementRef<HTMLInputElement>;
constructor(private modal: ModalService, private fb: FormBuilder) {
this.display$ = this.modal.watch();
}
ngAfterViewInit() {
fromEvent(this.searchDevice.nativeElement, 'input')
.pipe(
debounceTime(1000),
distinctUntilChanged(),
map((e: Event) => (e.target as HTMLInputElement).value)
)
.subscribe({
next: (res) => {
this.deviceDetails = null;
this.requestDeviceList.emit(res);
},
error: (err) => console.log(err),
});
}
- Facade
@Injectable()
export class PostingFacade {
constructor(private readonly store: Store<PostingState>) {}
public readonly postingData$: Observable<Posting[] | null> = this.store.pipe(
select(postingSelectors.getPostings)
);
public readonly devicesData$: Observable<Device[] | null> = this.store.pipe(
select(postingSelectors.getPostingDevices)
);
public initPostingsData(): void {
this.store.dispatch(postingActions.PostingInit({ limit: 18, offset: 0 }));
}
public loadMorePostings(limit: number, offset: number): void {
this.store.dispatch(postingActions.PostingLoadMore({ limit, offset }));
}
public queryPostings(query: string, limit: number, offset: number): void {
this.store.dispatch(postingActions.PostingSearch({ query, limit, offset }));
}
public queryDevices(query: string, limit: number): void {
this.store.dispatch(postingActions.PostingLoadDevices({ query, limit }));
}
public getDeviceDetails(key: string): void {
this.store.dispatch(postingActions.PostingLoadDeviceDetails({ key }));
}
}
- Effects
public readonly getPostings$: Observable<any> = createEffect(() =>
this.actions$.pipe(
ofType(PostingActionNames.PostingInit),
map(({ limit, offset }) => PostingActions.PostingInit({ offset, limit })),
switchMap(({ limit, offset }) =>
this.postingService
.getPostings(limit, offset)
.pipe(
map((data: Posting[]) => PostingActions.PostingInitSuccess({ data }))
)
),
catchError((error: string | null) =>
of(PostingActions.PostingInitFailure({ error }))
)
)
);
public readonly searchDevices$: Observable<any> = createEffect(() =>
this.actions$.pipe(
ofType(PostingActionNames.PostingLoadDevices),
map(({ query, limit }) =>
PostingActions.PostingLoadDevices({ query, limit })
),
switchMap(({ query, limit }) =>
this.postingService
.searchDevices(query, limit)
.pipe(
map((data: Device[]) =>
PostingActions.PostingLoadDevicesSuccess({ data })
)
)
),
catchError((error: string | null) =>
of(PostingActions.PostingLoadDevicesFailure({ error }))
)
)
);
- Reducer
export const _postingReducer = createReducer(
initialPostingState,
on(postingActions.PostingInit, (state) => ({
...state,
loaded: false,
error: null,
})),
on(postingActions.PostingInitSuccess, (state, { data }) => ({
...state,
postings: data,
loaded: true,
error: null,
})),
on(postingActions.PostingInitFailure, (state, { error }) => ({
...state,
loaded: false,
error,
})),
on(postingActions.PostingLoadDevices, (state) => ({
...state,
loaded: false,
error: null,
})),
on(postingActions.PostingLoadDevicesSuccess, (state, { data }) => ({
...state,
devices: data,
loaded: true,
error: null,
})),
on(postingActions.PostingLoadDevicesFailure, (state, { error }) => ({
...state,
loaded: false,
error,
})),
}
export function postingReducer(
state: PostingState | undefined,
action: Action
) {
return _postingReducer(state, action);
}
- Selectors
export const getPostings = createSelector(
getPostingState,
(state: PostingState) => state.postings
);
export const getPostingDevices = createSelector(
getPostingState,
(state: PostingState) => state.devices
);
- Services
getPostings(limit: number, offset: number): Observable<any> {
this.http
.post(
`${server}/postings/list`,
{ limit, offset },
{
headers: this.headers,
}
)
.subscribe((res) => this.request.next(res));
return this.request;
}
searchDevices(query: string, limit: number): Observable<any> {
this.http
.post(
`${server}/device/list/search/?query=${query}`,
{ limit },
{
headers: this.headers,
}
)
.subscribe((res) => this.request.next(res));
return this.request;
}
- State
export const POSTING_FEATURE_KEY = 'posting';
export const initialPostingState: PostingState = {
postings: null,
postingsDetails: null,
devices: null,
devicesDetails: null,
create: null,
user: null,
loaded: false,
error: null,
};
export interface PostingState {
postings: Posting[] | null;
postingsDetails: Posting | null;
devices: Device[] | null;
devicesDetails: Device | null;
create: Posting | null;
user: User | null;
loaded: boolean;
error: null | string;
}
CodePudding user response:
There are 2 main issues in the code:
1. You never unsubscribe in your components
You create memory leaks and callbacks keep running. Please use methods like take
/takeUntil
rxjs operators or async
pipe.
2. You have some kind of request subject
getPostings(limit: number, offset: number): Observable<any> {
this.http
.post(
`${server}/postings/list`,
{ limit, offset },
{
headers: this.headers,
}
)
.subscribe((res) => this.request.next(res));
return this.request;
}
searchDevices(query: string, limit: number): Observable<any> {
this.http
.post(
`${server}/device/list/search/?query=${query}`,
{ limit },
{
headers: this.headers,
}
)
.subscribe((res) => this.request.next(res));
return this.request;
}
Those lines are just weird and so wrong. You really should remove that subject, type the requests and return the data directly. Currently you will subscribe to an subject both responses are stored into which leads to your behavior.
Those methods should look like this:
getPostings(limit: number, offset: number): Observable<Posting[]> {
return this.http.post<Posting[]>(
`${server}/postings/list`,
{ limit, offset },
{ headers: this.headers },
);
}
searchDevices(query: string, limit: number): Observable<Device[]> {
return this.http.post<Device[]>(
`${server}/device/list/search/?query=${query}`,
{ limit },
{ headers: this.headers },
);
}