NGRX中统一管理文章列表并按ID加载单篇文章的可行性问询
Absolutely, this approach is not just reasonable—it’s a best practice for NGRX state management, especially as your application scales. Moving from individual properties for each specific article to a centralized list (indexed by ID) solves the redundancy problem you’re facing and aligns with how NGRX is designed to handle collections of entities.
Here’s why this pattern works so well, plus practical examples to implement it:
Key Benefits of This Approach
- Eliminates Redundant Code: Instead of writing duplicate actions (like
LoadArticle1,LoadArticle2), effects, and reducer cases for each article, you’ll have a single set of logic that works for any article ID. This makes your codebase cleaner and easier to maintain. - Scalable State Shape: If you ever need to add more articles to your page (or other parts of the app), you won’t have to modify your state structure, actions, or reducers—just dispatch the same action with a new ID.
- Efficient Data Access: Indexing articles by ID (using a dictionary/object) lets you look up specific articles in O(1) time, which is more efficient than filtering an array every time you need a single item.
Practical Implementation Example
1. Define the State Structure
Use an entity-based state to store articles, along with loading/error states tracked per ID:
interface Article { id: string; title: string; content: string; // other article properties } interface ArticlesState { entities: { [id: string]: Article }; // Index articles by ID for quick lookup ids: string[]; // Optional: Maintain order if you need to render articles in sequence loading: { [id: string]: boolean }; // Track loading state per article error: { [id: string]: string | null }; // Track errors per article } const initialState: ArticlesState = { entities: {}, ids: [], loading: {}, error: {} };
2. Create Reusable Actions
Replace your per-article actions with a single set of actions that accept an article ID:
import { createAction, props } from '@ngrx/store'; export const loadArticle = createAction( '[Articles] Load Article', props<{ id: string }>() ); export const loadArticleSuccess = createAction( '[Articles] Load Article Success', props<{ article: Article }>() ); export const loadArticleFailure = createAction( '[Articles] Load Article Failure', props<{ id: string; error: string }>() );
3. Build a Single Reducer
Your reducer will handle all article-related state changes with generic logic, no per-article cases:
import { createReducer, on } from '@ngrx/store'; import * as ArticleActions from './article.actions'; export const articlesReducer = createReducer( initialState, on(ArticleActions.loadArticle, (state, { id }) => ({ ...state, loading: { ...state.loading, [id]: true }, error: { ...state.error, [id]: null } })), on(ArticleActions.loadArticleSuccess, (state, { article }) => ({ ...state, entities: { ...state.entities, [article.id]: article }, ids: state.ids.includes(article.id) ? state.ids : [...state.ids, article.id], loading: { ...state.loading, [article.id]: false } })), on(ArticleActions.loadArticleFailure, (state, { id, error }) => ({ ...state, loading: { ...state.loading, [id]: false }, error: { ...state.error, [id]: error } })) );
4. Reuse Effects for Loading
Your effect will handle loading any article by ID, no need for duplicate effects:
import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, map, mergeMap } from 'rxjs/operators'; import * as ArticleActions from './article.actions'; import { ArticleService } from '../services/article.service'; @Injectable() export class ArticleEffects { loadArticle$ = createEffect(() => this.actions$.pipe( ofType(ArticleActions.loadArticle), mergeMap(({ id }) => this.articleService.getArticle(id).pipe( map((article) => ArticleActions.loadArticleSuccess({ article })), catchError((error) => of(ArticleActions.loadArticleFailure({ id, error: error.message }))) ) ) ) ); constructor(private actions$: Actions, private articleService: ArticleService) {} }
5. Select Specific Articles in Your Smart Component
Use memoized selectors to fetch the two specific articles you need:
import { createSelector } from '@ngrx/store'; import { AppState } from '../app.state'; import { ArticlesState } from './article.state'; // Base selector for the articles state const selectArticlesState = (state: AppState) => state.articles; // Selector to get an article by ID export const selectArticleById = (id: string) => createSelector(selectArticlesState, (state: ArticlesState) => state.entities[id]); // In your smart component import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { Article } from './article.model'; import * as ArticleActions from './article.actions'; import { selectArticleById } from './article.selectors'; @Component({ selector: 'app-article-page', templateUrl: './article-page.component.html' }) export class ArticlePageComponent implements OnInit { article1$: Observable<Article | undefined>; article2$: Observable<Article | undefined>; constructor(private store: Store) { this.article1$ = this.store.select(selectArticleById('article-1-id')); this.article2$ = this.store.select(selectArticleById('article-2-id')); } ngOnInit() { // Dispatch actions to load the two articles this.store.dispatch(ArticleActions.loadArticle({ id: 'article-1-id' })); this.store.dispatch(ArticleActions.loadArticle({ id: 'article-2-id' })); } }
Final Notes
Even if you only need two articles right now, this pattern future-proofs your code. If you later need to add more articles, or reuse article-loading logic elsewhere in the app, you won’t have to refactor your entire state setup. It also keeps your reducer logic focused on updating the entity collection, which aligns with NGRX’s core principles of single-responsibility reducers.
内容的提问来源于stack exchange,提问作者Ondra Piza

