In my development journey with Angular, I've noticed a recurring pattern that I find myself utilizing regularly:
The approach you're employing bears resemblance to what is typically found in a state store like; Redux, NgRX, or NGXS. The difference lies in consolidating the store, selectors, and reducers within a singular class.
Centralizing all elements has its advantages, but repeatedly reconstructing a new store for each service inception could explain why you feel the code becomes overly verbose and redundant.
This practice is not uncommon, as evident by numerous blog posts attempting to replicate Redux functionality in minimal lines of code. It's clear that many are traversing down the same path you are.
private readonly _widgets: BehaviorSubject<Widget[]> = new BehaviorSubject([]);
The aforementioned represents the store of the state manager. Serving as an observable that encapsulates the present state and signals alterations within that state.
public get widgets() { return this._widgets.value; }
This functions as the snapshot of the state manager, enabling specific computations on the store without necessitating subscription. However, similar to other state stores, employing snapshots may provoke race condition issues. Moreover, refrain from direct template access to avoid triggering the "Expression has changed after it was checked" error.
public readonly widgets$ = this._widgets.asObservable();
The above constitutes a selector for the store. Stores frequently encompass various selectors facilitating disparate parts of the application to monitor store modifications concerning specific subjects.
public add(widget: Widget) {
const old = this._widgets.value.slice();
old.push(widget);
this._widgets.next(old);
// alternately
this._widgets.next([...this.widgets, widget]);
}
Frameworks like state store libraries do not boast the structure illustrated above. This design dissects into two components - the action and the reducer. Generally, the action encompasses the payload (e.g., a widget), while the reducer executes modifications within the store.
Embracing actions and reducers isolates the business logic of store alteration from tasks such as reading the current state, updating, and preserving the ensuing state. While your example remains simplistic, cumbersome boilerplate code might arise during large-scale applications where subscribing, modifying, and emitting changes become encumbrance when toggling simple boolean flags.
Many services host 3-5 or more groupings featuring public getters and private backing Subjects. The pervasiveness often renders the code verbose and repetitive.
You appear to be treading in the territory of reinventing existing conventions.
Two feasible pathways lie ahead. Pioneering your custom state store framework conducive to your workflow, or integrating an established state store gleaned from the previously mentioned libraries. We cannot dictate the route to undertake, though based on my experience across numerous Angular projects, no definitive answer exists.
The means through which source code attains succinctness and reduces redundancy hinges heavily on personal stance. A procedure rendering code less verbose at present might eventually materialize as a flawed design choice, while repetitious code proves irksome yet evidently valuable when tweaking a solitary line sans impacting the remainder of the codebase.
a) Is there a DRY methodology available, and
To streamline source code, decouple state management implementation from business logic. This delves into identifying optimal design patterns for a state store.
- Do you engage selectors?
- Do you involve actions?
- Do you utilize reducers?
A multitude of queries pertain to personal preferences.
Illustratively reimagining your scenario using NGXS might not align with your vision of DRY due to frameworks inherently harboring complexity to deliver utility. Referencing NGXS documentation for unfamiliar endeavors proves more beneficial than embarking on solo creations susceptible to inaccuracies. Notwithstanding, faulting NGXS does engender consolation - it's beyond your confines :)
@State<Widget[]>({
name: 'widgets',
defaults: []
})
export class WidgetState {
@Action(AddWidgetAction)
public add(ctx: StateContext<Widget[]>, {payload}: AddWidgetAction) {
ctx.setState([...ctx.getState(), payload]);
}
}
@Component({...})
export class WidgetsComponent {
@Select(WidgetState)
public widgets$: Observable<Widget[]>;
public constructor(private _store: Store) {};
public clickAddWidget() {
this._store.dispatch(new AddWidgetAction(new Widget()));
}
}
b) Am I incorrectly leveraging Observables here?
Your utilization of observables demonstrates precision. You grasp why a service should maintain stateless and reactive attributes. It appears you're exploring the benefits of state stores independently and seeking methods to streamline their usage.