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

When using a Redux action type with an optional payload property, TypeScript may raise complaints within the reducer

In my react-ts project, I define the following redux action type: type DataItem = { id: string country: string population: number } type DataAction = { type: string, payload?: DataItem } I included an optional payload property because there are tim ...

Angular Integration Testing - API Validation

I am currently exploring Integration Tests in Angular and trying to grasp their importance. From my understanding, these tests allow us to verify the outcome of an API call to ensure that we are getting the expected result. However, I have come across tut ...

Navigating in Angular with parameters without modifying the URL address

In a nutshell, my goal is to navigate to a page with parameters without showing them in the URL. I have two components: Component A and B. What I want to do is route to B while still needing some parameters from A. I know I can achieve this by setting a ...

typescript defining callback parameter type based on callback arguments

function funcOneCustom<T extends boolean = false>(isTrue: T) { type RETURN = T extends true ? string : number; return (isTrue ? "Nice" : 20) as RETURN; } function funcCbCustom<T>(cb: (isTrue: boolean) => T) { const getFirst = () => ...

The combination of Angular's *ngIf directive and ng-template is causing issues

When I have up to 3 icons, I require less space compared to when I have 3 icons or more. To address this issue, I have implemented the following code that utilizes both *ngIf and ng-template. Unfortunately, the implementation is not functioning as expect ...

Typescript: object containing at least one property with the type T assigned

Is there a method to write the HasNumber interface in Typescript without receiving an error due to the fact that the HasNumberAndString interface includes a property that is not of type number? I am looking for a way to require the HasNumberAndString int ...

Retrieving the selector name from a JSON in Angular 2

I'm looking to create a dynamic form where element details will be sourced from JSON data. This is the JSON structure I have: { "FormElements": [ { "selectorName": "text-input", "id": "", "class": "location", "nam ...

Angular universal triggers an "Error at XMLHttpRequest.send" issue

After updating my project to Angular 10 and incorporating angular universal, I encountered a strange error. While the application builds without any issues, I face an error when trying to run it on my development environment: ERROR Error at XMLHttpReque ...

Why is TS1005 triggered for Redux Action Interface with an expectation of '=>'?

I'm finding it difficult to identify what's causing this issue, as shown in the esLint error from Typescript displayed in the screenshot below: https://i.stack.imgur.com/pPZa7.png Below is the complete code, which consists of actions for redux. ...

Neglect variables that have not been declared (TypeScript)

Currently, I am working on developing a WebExtension using TypeScript that will be later compiled into JavaScript. This extension relies on one of the browser APIs offered by Firefox, specifically the extension API. An example of this is my use of the get ...

Tips for updating the styles within a class for Angular 6 dynamically

Currently, I am able to update the button design using ng-container. Here is a snippet of the code: <ng-container *ngIf="isDisabled;"> <button class="bot-btn-disabled" (click)="confirm()" [disabled]=this. ...

Talebook by Syncfusion

I'm completely new to Storybook and I am currently exploring the possibility of creating a Storybook application that showcases a variety of controls, including Syncfusion controls and other custom controls that I will be developing in the future. Ha ...

Leveraging generics within TypeScript

I have developed a class in TypeScript that uses generics. export class ModelTransformer { static hostelTransformer: HostelTransformer; static fromAtoB(instance: T): U { if (instance instanceof HostelType) { return ModelTrans ...

The issue of the list view button not responding to click events in Angular2 NativeScript

I have been working on implementing an onclick event for a listview item button. Below is the code snippet that I have tried, but unfortunately the console log is not being triggered when the button is clicked. I am unsure of what the problem might be in ...

Utilizing TypeScript to mandate properties in a React component

When trying to apply TypeScript type enforcement on React components, I encountered some unexpected behavior. Here are simplified examples: function FunctionalComponent(props: { color: string }) { return <></>; } type ComponentWithName<I ...

Upgrading Angular causes issues with fileReplacements not functioning properly for assets

Since upgrading Angular, I have encountered an issue where fileReplacements no longer work with assets. Here is the code snippet that I am using: "fileReplacements": [ { "replace": "src/assets/scss/x.scss", ...

I am looking for guidance on removing the bottom line from the ionic 4 segment indicator. Any advice or tips on

.segment-button-indicator { -ms-flex-item-align: end; align-self: flex-end; width: 100%; height: 2px; background-color: var(--indicator-color); opacity: 1; } I am a beginner in hybrid app development and ...

What sets apart calling an async function from within another async function? Are there any distinctions between the two methods?

Consider a scenario where I have a generic function designed to perform an upsert operation in a realmjs database: export const doAddLocalObject = async <T>( name: string, data: T ) => { // The client must provide the id if (!data._id) thr ...

What is the best way to broaden the capabilities of function objects through the use of

Below is a code snippet that raises the question of how one should define certain types. In this scenario, it is required that Bar extends Foo and the return type of FooBar should be 'a'. interface Foo { (...args: any):any b: string } i ...

Vue3 can accept a prop of type String or PropType

In my Vue3 project, I have a component that accepts a prop which can be either a string or an object. Here's how it looks: import { defineComponent } from 'vue' const Component = defineComponent({ props: { book: { type: [String, ...