Could the repeated utilization of BehaviorSubject within Angular services indicate a cause for concern?

While developing an Angular application, I've noticed a recurring pattern in my code structure:

@Injectable(...)
export class WidgetRegsitryService {
  private readonly _widgets: BehaviorSubject<Widget[]> = new BehaviorSubject([]);
  public get widgets() { return this._widgets.value; }
  public readonly widgets$ = this._widgets.asObservable();

  public add(widget: Widget) {
    const old = this._widgets.value.slice();
    old.push(widget);
    this._widgets.next(old);
  }
}

I often find myself implementing multiple groups of public getters and private Subjects in various services, leading to verbose and repetitive code. This begs the questions: a) Is there a more DRY approach to handle this repetition? and b) Am I potentially misusing Observables in this context?

Answer №1

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.

Answer №2

A) One solution to prevent repetitive code in creating a BehaviorSubject is by creating a single BehaviorSubject containing a key-value pair. This way, we can subscribe using the key instead of creating a new BehaviorSubject every time it's needed.

Data Exchange Service

interface Event {
  key: string;
  value: any;
}


@Injectable({
  providedIn: 'root'
})

export class Broadcaster {

  // subject 
  protected _eventsSubject = new BehaviorSubject<Event>();
  constructor() {
  }

   broadcast(key: any, value: any) {
    this._eventsSubject.next({ key, value }); // setting the key and value of the subject
   }

  on<T>(key: any): Observable<T> {
    return this._eventsSubject.asObservable()
            .pipe(
                filter(e => e.key === key),
                map(e => e.value)
            );
  }
}

ComponentOne

import { Broadcaster } from '../BrodcastService.service';
export class ComponentOne implements OnInit {
constructor(private broadcaster: Broadcaster) { }

someFunction() {

// sending data and setting the key of the subject to 'msg1'

this.broadcaster.broadcast('msg1', 'data of msg1');

}

ComponentTwo

import { Broadcaster } from '../BrodcastService.service';
export class ComponentOne implements OnInit {
constructor(private broadcaster: Broadcaster) { }

someFunction() {

// sending data and setting the key of the subject to 'msg2'

this.broadcaster.broadcast('msg2', 'data of msg2');
}

ComponentThree

import { Broadcaster } from '../BrodcastService.service';
export class ComponentOne implements OnInit {
constructor(private broadcaster: Broadcaster) { }

someFunction() {
// subscribing to the subject to retrieve the value for key 'msg1'
    this.broadcaster.on('msg1').subscribe(resp => {
      console.log(resp);
    })
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Bring in a library with Angular2 using webpack

I am currently utilizing the angular2-webpack starter from GitHub, and I am looking to incorporate an npm library, such as Babylon JS. My approach so far has been as follows: import * as BABYLON from 'babylonjs/babylon'; The Babylon library inc ...

What is the reasoning behind an empty input value being considered as true?

I am facing an issue with the following code that is supposed to execute oninput if the input matches the answer. However, when dealing with a multiplication problem that equals 0, deleting the answer from the previous calculation (leaving the input empt ...

Having trouble with UpdateMany in mongoose, but it works perfectly in mongodb when executed directly

I am attempting to update and insert data in mongoose. After sending some requests, I receive the following result: let obj = [ {name: aaa, age: 10}, {name: bbb, age: 11}, {name: ccc, age: 12}, ] My goal is to update all existing documents an ...

A guide to building a versatile higher-order function using TypeScript

I'm struggling with creating a function that can add functionality to another function in a generic way. Here's my current approach: /** * Creates a function that first calls originalFunction, followed by newFunction. * The created function re ...

Using createStyles in TypeScript to align content with justifyContent

Within my toolbar, I have two icons positioned on the left end. At the moment, I am applying this specific styling approach: const useStyles = makeStyles((theme: Theme) => createStyles({ root: { display: 'flex', }, appBar: ...

Vanilla JavaScript error: Unable to access property

I am working on implementing a header with a logo and navigation that includes a menu toggle link for smaller viewports. My goal is to achieve this using Vanilla JS instead of jQuery. However, when I click on the menu toggle link, I encounter the followin ...

Amazon Banner Integration for Angular Version 4

Having some trouble getting an Amazon banner to display inside an angular material 2 card. The div appears empty and the banner is not rendering. Any idea what could be causing this issue? Below is the code snippet showcasing my attempts: <md-card clas ...

Tips on using constructor functions and the new keyword in Typescript

This is a demonstration taken from the MDN documentation showcasing the usage of the new keyword function Car(make, model, year) { this.make = make; this.model = model; this.year = year; } const car1 = new Car('Eagle', 'Talon TSi&apos ...

Fire the BehaviorSubject with the identical value following a mutation

I am working with a BehaviorSubject where I have to make changes through mutation (for reasons beyond my control). I need to trigger the BehaviorSubject for subscriptions whenever there are changes. Is there another approach I can take instead of using: ...

Exploring Angular 2: Diving into Validators, ReactiveForms, FormBuilder, and tailored classes

If you have a class called User with properties username and password export class User { username = ''; password = ''; } To create a reactive form, you can use the following syntax this.userForm = this.fb.group({ usernam ...

Having trouble integrating SVG icons into my Angular project

I'm encountering an issue with adding icons that I've designed in Figma to my web application. Some of the icons are displaying correctly while others are not. Here is the code snippet I am using to add the icons: .dx-icon-info { mask-image: ...

Validation of dynamic fields in a reactive form is malfunctioning specifically for the <Select> element

My form includes a reactive form with a form array, where clicking a button adds a new form group to the array. Upon form submission, I want to validate if these dynamic fields are empty. However, room.errors?.required does not seem to execute as expected ...

Jest | Testing Tool for Functions with Multiple Parameters

I have developed a new tool that requires 3 parameters: value (string), service (any - an actual GET service), and name (string). Below is the code for the tool: import { serverErrorResponseUtil } from 'util/serverErrorResponseUtil'; import { H ...

Retrieve the output of forkJoin subscription in Angular 6 using rxJs 6

A Straightforward Example: const tasks = []; for (let i = 0; i < this.initialData.length; i++) { tasks.push( this.taskService.getDetails(this.id1[i], this.id2[i]) }; combineLatest(...tasks).subscribe(taskGroup => { console.log(task ...

What is the most effective way to loop and render elements within JSX?

Trying to achieve this functionality: import React from 'react'; export default class HelloWorld extends React.Component { public render(): JSX.Element { let elements = {"0": "aaaaa"}; return ( ...

Utilizing Ionic 4 to navigate within the lifecycle event, ensuring page does not display upon reloading

I am facing an issue on a particular page that should only be accessible to logged-in users. If a not-logged-in user tries to access the page directly, I want to redirect them to the login page. Currently, I have implemented the following logic: ionViewWi ...

Angular 2 interprets my JSON object as a function

My webapp retrieves data via Json and places it in objects for display. I have successfully implemented this process twice using services and classes. However, when I recently copied and pasted the old code with some modifications to redirect to the correc ...

Preventing special characters in an input field using Angular

I am trying to ensure that an input field is not left blank and does not include any special characters. My current validation method looks like this: if (value === '' || !value.trim()) { this.invalidNameFeedback = 'This field cannot ...

Creating Typescript type definitions for any object properties

Struggling to find the correct TS syntax with Typescript 3.7.3. I have a random object, for example: var obj = { one: ..., two: ... three: ... }; I want to create a type that includes all keys from that object, like this: type ObjKeys = &ap ...

Is there a way to display the text that has been selected?

I would like to display the text of a clicked link in an empty div element. Can someone help me achieve this? Thank you. <div></div> <span>link one</span> <span>link two</span> <span>link three</span> ...