You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NgRx Store多并行请求状态组织方案咨询:多书籍展开时的独立状态管理

NgRx Store 多实例请求状态管理方案

针对你遇到的多书籍卡片独立请求状态跟踪问题,我推荐采用以书籍ID为键的字典式状态结构来管理每个书籍的summary和translations请求状态,这样既能保持状态的清晰组织,又能灵活处理动态新增的请求实例。以下是具体的实现方案:

1. 重构核心状态接口

首先扩展你原有的bookState,新增两个字典(Record)类型的字段,分别存储每个书籍的摘要和翻译状态。这样可以通过书籍ID快速定位对应的请求状态:

// 调整子状态接口(可选:去掉bookId字段,因为字典key已经是bookId)
interface BookSummaryState {
  summary: ISummary | null;
  isRequesting: boolean;
  error: IError | null;
}

interface BookTranslationsState {
  translations: Translations[] | null;
  isRequesting: boolean;
  error: IError | null;
}

// 重构后的核心状态
interface BookState {
  books: IBook[];
  // 以bookId为key存储每个书籍的摘要状态
  summaries: Record<string, BookSummaryState>;
  // 以bookId为key存储每个书籍的翻译状态
  translations: Record<string, BookTranslationsState>;
  // 可选:全局加载状态(如果需要统一显示遮罩等)
  globalIsRequesting: boolean;
  // 可选:全局错误(或依赖子状态的错误聚合)
  globalError: IError | null;
}

初始化状态时,summaries和translations可以设为空对象:

const initialState: BookState = {
  books: [],
  summaries: {},
  translations: {},
  globalIsRequesting: false,
  globalError: null
};

2. 定义对应的Action

为每个请求流程创建Action,包含书籍ID参数,以便Reducer准确定位要更新的状态:

// 摘要相关Action
export const loadBookSummary = createAction(
  '[Book] Load Summary',
  props<{ bookId: string }>()
);
export const loadBookSummarySuccess = createAction(
  '[Book] Load Summary Success',
  props<{ bookId: string; summary: ISummary }>()
);
export const loadBookSummaryFailure = createAction(
  '[Book] Load Summary Failure',
  props<{ bookId: string; error: IError }>()
);

// 翻译相关Action(结构类似)
export const loadBookTranslations = createAction(
  '[Book] Load Translations',
  props<{ bookId: string }>()
);
export const loadBookTranslationsSuccess = createAction(
  '[Book] Load Translations Success',
  props<{ bookId: string; translations: Translations[] }>()
);
export const loadBookTranslationsFailure = createAction(
  '[Book] Load Translations Failure',
  props<{ bookId: string; error: IError }>()
);

// 可选:清理单个书籍的状态(比如折叠卡片时)
export const clearBookExtraState = createAction(
  '[Book] Clear Extra State',
  props<{ bookId: string }>()
);

3. 实现Reducer逻辑

在Reducer中处理上述Action,针对不同的书籍ID更新对应的字典条目:

const bookReducer = createReducer(
  initialState,
  // 处理摘要加载中
  on(loadBookSummary, (state, { bookId }) => ({
    ...state,
    summaries: {
      ...state.summaries,
      [bookId]: {
        ...state.summaries[bookId],
        isRequesting: true,
        error: null
      }
    },
    globalIsRequesting: true
  })),
  // 处理摘要加载成功
  on(loadBookSummarySuccess, (state, { bookId, summary }) => ({
    ...state,
    summaries: {
      ...state.summaries,
      [bookId]: {
        summary,
        isRequesting: false,
        error: null
      }
    },
    globalIsRequesting: checkGlobalRequesting(state)
  })),
  // 处理摘要加载失败
  on(loadBookSummaryFailure, (state, { bookId, error }) => ({
    ...state,
    summaries: {
      ...state.summaries,
      [bookId]: {
        ...state.summaries[bookId],
        isRequesting: false,
        error
      }
    },
    globalIsRequesting: checkGlobalRequesting(state),
    globalError: error
  })),
  // 翻译相关Reducer逻辑与摘要完全类似,此处省略...
  // 清理单个书籍的额外状态
  on(clearBookExtraState, (state, { bookId }) => ({
    ...state,
    summaries: omit(state.summaries, bookId),
    translations: omit(state.translations, bookId)
  }))
);

// 辅助函数:检查是否有任何请求在进行
function checkGlobalRequesting(state: BookState): boolean {
  const hasSummaryRequest = Object.values(state.summaries).some(s => s.isRequesting);
  const hasTranslationRequest = Object.values(state.translations).some(t => t.isRequesting);
  return hasSummaryRequest || hasTranslationRequest;
}

4. 编写Selector获取特定书籍状态

创建Selector来快速获取指定书籍ID的摘要或翻译状态,组件中可以直接使用这些Selector订阅数据:

// 基础Feature Selector
export const selectBookState = createFeatureSelector<BookState>('books');

// 获取所有摘要状态
export const selectAllSummaries = createSelector(
  selectBookState,
  state => state.summaries
);

// 获取指定书籍ID的摘要状态(默认返回初始状态避免空值)
export const selectSummaryByBookId = (bookId: string) => createSelector(
  selectAllSummaries,
  summaries => summaries[bookId] || { summary: null, isRequesting: false, error: null }
);

// 翻译相关Selector与摘要完全类似,此处省略...

// 全局加载状态Selector
export const selectGlobalIsRequesting = createSelector(
  selectBookState,
  state => state.globalIsRequesting
);

5. 实现Effects处理HTTP请求

使用NgRx Effects处理异步HTTP请求,确保每个请求的结果正确分发到Reducer:

@Injectable()
export class BookEffects {
  loadSummary$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadBookSummary),
      // 防止重复请求:如果当前书籍摘要正在加载,则跳过
      withLatestFrom(this.store.select(selectSummaryByBookId(action => action.bookId))),
      filter(([action, summaryState]) => !summaryState.isRequesting),
      switchMap(([action]) =>
        this.bookService.getBookSummary(action.bookId).pipe(
          map(summary => loadBookSummarySuccess({ bookId: action.bookId, summary })),
          catchError(error => of(loadBookSummaryFailure({ bookId: action.bookId, error })))
        )
      )
    )
  );

  loadTranslations$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadBookTranslations),
      withLatestFrom(this.store.select(selectTranslationsByBookId(action => action.bookId))),
      filter(([action, translationState]) => !translationState.isRequesting),
      switchMap(([action]) =>
        this.bookService.getBookTranslations(action.bookId).pipe(
          map(translations => loadBookTranslationsSuccess({ bookId: action.bookId, translations })),
          catchError(error => of(loadBookTranslationsFailure({ bookId: action.bookId, error })))
        )
      )
    )
  );

  constructor(
    private actions$: Actions,
    private store: Store,
    private bookService: BookService
  ) {}
}

6. 组件中的使用示例

在书籍卡片组件中,通过Selector订阅当前书籍的状态,根据状态显示加载、错误或数据:

@Component({...})
export class BookCardComponent {
  @Input() book: IBook;

  summaryState$ = this.store.select(selectSummaryByBookId(this.book.id));
  translationsState$ = this.store.select(selectTranslationsByBookId(this.book.id));

  constructor(private store: Store) {}

  onExpand(): void {
    // 触发两个独立请求
    this.store.dispatch(loadBookSummary({ bookId: this.book.id }));
    this.store.dispatch(loadBookTranslations({ bookId: this.book.id }));
  }

  onCollapse(): void {
    // 可选:清理当前书籍的额外状态
    this.store.dispatch(clearBookExtraState({ bookId: this.book.id }));
  }
}

模板中可以这样处理状态:

<div class="book-card" (click)="onExpand()">
  <!-- 书籍基础信息 -->
  <h3>{{ book.title }}</h3>

  <!-- 摘要区域 -->
  <div *ngIf="summaryState$ | async as summaryState">
    <div *ngIf="summaryState.isRequesting">加载摘要中...</div>
    <div *ngIf="summaryState.error">加载失败:{{ summaryState.error.message }}</div>
    <div *ngIf="summaryState.summary">{{ summaryState.summary.content }}</div>
  </div>

  <!-- 翻译区域类似 -->
</div>

方案优势

  • 状态隔离:每个书籍的请求状态独立存储,互不干扰
  • 动态扩展:无需预先定义状态结构,新增书籍时自动添加对应的状态条目
  • 性能优化:通过Selector精准订阅单个书籍的状态,避免不必要的重渲染
  • 可维护性:状态结构清晰,Action、Reducer、Selector职责明确

内容的提问来源于stack exchange,提问作者user1380431

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 12:47:34