【问题标题】:NGRX Action that depends on other observable(selector) values依赖于其他可观察(选择器)值的 NGRX 操作
【发布时间】:2022-01-27 04:11:21
【问题描述】:

我遇到了一种情况,我需要从 2 个独立的数据源中获取数据。一个数据源依赖于来自存储中其他值的 2 个值。

在下面的代码示例中,我需要获取“银行”列表并呈现它们。然后在将来的某个时候,我需要“登录”并获取这些银行的“余额”列表。

我在依赖部分遇到问题。我可以手动订阅 observables 并且它可以工作,但我认为这不是正确的方法。我怀疑正确的方法是 rxjs 运算符和选择器的某种组合。我会很感激一些方向。

组件

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { getBalancesAction, getBanksAction, loginAction } from './store/app.actions';
import { selectBalances, selectBanks, selectLogin } from './store/app.selectors';
import { IAppState } from './store/app.state';
import * as _ from 'lodash';
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit
{

    // Desired behavior
    // Banks render right off the bat.
    // CLicking login

    public banks$ = this.store.select(selectBanks);
    public login$ = this.store.select(selectLogin);
    public balances$ = this.store.select(selectBalances);

    constructor(private store: Store<IAppState>)
    {

    }

    ngOnInit()
    {
        this.store.dispatch(getBanksAction());

        // You can view the list of banks without being logged in
        // I need to call this.store.dispatch(getBalances(banks$, login$))
        // but only after I have a login and the list of banks.
        // I could subscribe, but that doesn't seem to be in the spirit of ngrx and rxjs.
        this.banks$.subscribe(banks =>
        {
            if (banks)
            {
                this.login$.subscribe(login =>
                {
                    if (login)
                    {
                        // getting balances requires the accountid and a list of banks.  These come from observables of their own.
                        this.store.dispatch(getBalancesAction({ accountId: login, banks: _.map(banks, b => b.id) }))
                    }
                })
            }
        });


    }

    public login(): void
    {
        this.store.dispatch(loginAction({ userName: 'justme' }));
    }
}

动作

import { createAction, props } from "@ngrx/store";
import { IBalance, IBank } from "./app.state";

export enum BankActions
{
    GET_BANKS = "GET_BANKS",
    GET_BANKS_SUCCESS = "GET_BANKS_SUCCESS",
    GET_BALANCES = "GET_BALANCES",
    GET_BALANCE_SUCCESS = "GET_BALANCES_SUCCESS",
    LOGIN = "LOGIN",
    LOGIN_SUCCESS = "LOGIN_SUCCESS"
}

export const getBanksAction = createAction(
    BankActions.GET_BANKS
);

export const getBanksSuccessAction = createAction(BankActions.GET_BANKS_SUCCESS, props<{ banks: IBank[] }>());


export const getBalancesAction = createAction(
    BankActions.GET_BALANCES,
    props<{ accountId: string, banks: string[] }>()
);

export const getBalancesSuccessAction = createAction(BankActions.GET_BALANCE_SUCCESS, props<{ balances: IBalance[] }>());

export const loginAction = createAction(
    BankActions.LOGIN,
    props<{userName: string}>()
)

export const loginSuccessAction= createAction(
    BankActions.LOGIN_SUCCESS,
    props<{accountId: string}>()
)

效果

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { defer, forkJoin, from, of } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators';
import { BanksService } from '../banks.service';
import { BankActions, getBalancesSuccessAction, getBanksSuccessAction, loginSuccessAction } from './app.actions';

@Injectable()
export class BankEffects
{
    constructor(private actions$: Actions, private banksService: BanksService) { }

    loadBalances$ = createEffect(() => this.actions$.pipe(
        ofType(BankActions.GET_BALANCES),
        mergeMap((x:any) => this.banksService.getBalances(x.accountId, x.banks)
            .pipe(
                map(b => getBalancesSuccessAction({ balances: b }))
            ))
    )
    );

    loadBanks$ = createEffect(() => this.actions$.pipe(
        ofType(BankActions.GET_BANKS),
        mergeMap(() => this.banksService.getBanks()
            .pipe(
                map(b => getBanksSuccessAction({ banks: b }))
            ))
    )
    );

    doLogin$ = createEffect(() => this.actions$.pipe(
        ofType(BankActions.LOGIN),
        mergeMap((x) => this.banksService.login(x)
            .pipe(
                map(b => loginSuccessAction({ accountId: b }))
            ))
    )
    );
}

减速器

import { createReducer, on } from "@ngrx/store";
import { getBalancesSuccessAction, loginSuccessAction, getBanksSuccessAction } from "./app.actions";
import { IAppState, initialState } from "./app.state";

export const appReducer = createReducer(
    initialState,
    on(getBalancesSuccessAction, (state: IAppState, { balances }) => ({ ...state, balances: balances })),
    on(loginSuccessAction, (state: IAppState, { accountId }) => ({ ...state, accountId: accountId })),
    on(getBanksSuccessAction, (state: IAppState, { banks }) =>
    {
        return ({ ...state, banks: banks });
    }));

选择器

import { createSelector } from "@ngrx/store";
import { IAppState } from "./app.state";

export const selectFeature = (state: any) => state.app;

export const selectLogin = createSelector(
    selectFeature,
    (state: IAppState) => state.accountId
);

export const selectBanks = createSelector(
    selectFeature,
    (state: IAppState) =>
    {
        return state.banks;
    }
);

export const selectBalances = createSelector(
    selectFeature,
    (state: IAppState) => state.balances
);

状态

export interface IBank
{
    id: string;
    name: string;
}

export interface IBalance{
    bankId: string,
    value: number;
    internalAccountNumber: string
}

export interface IAppState
{
    banks: IBank[],
    accountId: string | null,
    balances: IBalance[]
}

export const initialState: IAppState = {
    banks: [],
    accountId: null,
    balances: []
};

【问题讨论】:

    标签: angular ngrx ngrx-store


    【解决方案1】:
    1. 您的ngOnInit 应该只发送BankActions.GET_BANKS。它不应包含任何subscribes
    2. 在模板中使用async管道订阅banks$balances$
    3. loadBalances$应该由BankActions.LOGIN_SUCCESS触发,你可以使用withLatestFromhttps://stackoverflow.com/a/60287236/1188074)从效果内的商店中获取账户ID和银行列表
    4. 删除BankActions.GET_BALANCES
    5. 从您的组件中删除 login$

    参考文献

    连锁效应

    手动订阅

    【讨论】:

    • 在成功操作上触发效果通常是不好的做法。它创建了一个规范和隐藏的事件链。对于 OP 的问题,我不会特别推荐链接事件。看起来getBalancesAction 操作应该只触发一次(或者可能在他们的用户刷新页面时触发,但这是另一个问题)。
    • 因此,如果链接成功操作不好并且手动订阅不好,那么答案是什么。登录可以多次触发,因此会再次触发获取余额。
    • 在上面添加了引用并明确提及async 管道。回复getBalances - 无论登录次数如何,您是否希望它只触发一次?
    【解决方案2】:

    你可以使用combineLatest,因为它会产生所有 Observable 的值

    另外,map 函数不需要 loadash。

    combineLatest([this.banks$, this.login$])
        .pipe(
          // to prevent unwanted many yields on a short while
          debouncTime(50),
          // only yields when there are banks and the accountId is filled in.
          filter(([banks, accountId]) => banks?.length > 0 && accountId) 
        )
        .subscribe(([banks, accountId]) =>
          this.store.dispatch(getBalancesAction({ accountId, banks: banks.map(b => b.id) }))
        );
    

    【讨论】:

    • 手动订阅是一个 ngrx 反模式,就像传递商店中已经存在的信息一样 - 组件不是所有这些逻辑的地方。
    • @wlf 建议对组件订阅和相关效果进行完整重构是幼稚的。 OP 的源 sn-p 可能抑制了很多真实代码。这个答案是 OP 可以做的最小的代码更改,以简化他们的订阅,而无需任何其他修改
    • 相关评论添加到其他答案。
    猜你喜欢
    • 1970-01-01
    • 2019-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-29
    相关资源
    最近更新 更多