Ways to steer clear of utilizing subscriptions and BehaviorSubject.value through a declarative method within rxjs

As I refactor my Angular application, my goal is to eliminate all subscriptions and rely solely on the async pipe provided by Angular for a declarative approach instead of an imperative one.

I encounter difficulties implementing a declarative approach when multiple sources can trigger changes in the stream. If there was only one source, I could simply use the scan operator to accumulate my emitted values.

Scenario:

Imagine a simple component where an array of strings is resolved during routing. In this component, I aim to display the list and allow users to add or remove items using buttons.

Constraints:

  1. Avoiding the use of subscribe as I want Angular to handle unsubscription through the async pipe.
  2. Steering clear of BehaviorSubject.value as it feels more like an imperative approach rather than a declarative one.
  3. Avoiding any form of subject usage except for button click event propagation, believing that the necessary observables should be in place and just need to be connected together.

Current Progress:

My journey so far has gone through several stages, each with its own challenges:

  1. Using BehaviorSubject and .value to generate the new array – not truly declarative.
  2. Experimenting with the scan operator and creating an Action interface where each button emits an action type. This felt somewhat reminiscent of Redux but mixing different value types within one pipe felt awkward.
  3. My preferred approach thus far involves simulating a BehaviorSubject using shareReplay and instantly emitting the value into the button by transitioning to a new observable using concatMap, limiting it to 1 value to prevent creating a loop.

list-view.component.html:

<ul>
  <li *ngFor="let item of items$ | async; let i = index">
    {{ item }} <button (click)="remove$.next(i)">remove</button>
  </li>
</ul>

<button (click)="add$.next('test2')">add</button>

list-view.component.ts

// Define simple subjects for adding and removing items
add$ = new Subject<string>();
remove$ = new Subject<number>();

// Observable holding the actual displayed list
items$: Observable<string[]>;

constructor(private readonly _route: ActivatedRoute) {
// Define observable emitting resolver data

// Merge initial data, data on addition, and removal to bring the data to Subject
this.items$ = merge(
this._route.data.pipe(map(items => items[ITEMS_KEY])),

// Observable for adding items to the array
this.add$.pipe(
concatMap(added =>
this.items$.pipe(
map(list => [...list, added]),
take(1)
)
)
),

// Observable for removing items from the array
this.remove$.pipe(
concatMap(index =>
this.items$.pipe(
map(list => [...list.slice(0, index), ...list.slice(index + 1)]),
take(1)
)
)
)
).pipe(shareReplay(1));
}

Despite being what seems like a straightforward example, I find my implementation overly complex for such a task. Any guidance towards simplifying this process would be greatly appreciated.

You can access a StackBlitz demonstration of my implementation here: StackBlitz Demo

Answer №1

To manage modifications, you can establish a stream called modifications$ that captures each emission from your "modification subjects" and applies them to a function for state modification:

export class AppComponent {
  add$ = new Subject<string>();
  remove$ = new Subject<number>();

  private modifications$ = merge(
    this.add$.pipe(map(item => state => state.concat(item))),
    this.remove$.pipe(map(index => state => state.filter((_, i) => i !== index))),
  );

  private routeData$ = this.route.data.pipe(map(items => items[ITEMS_KEY]));

  items$ = this.routeData$.pipe(
    switchMap(items => this.modifications$.pipe(
      scan((state, fn) => fn(state), items),
      startWith(items)
    ))
  );

  constructor(private route: ActivatedRoute) { }
}

In this code snippet, the items$ is defined to initially use the route data, then switches to a stream that applies reducer functions to the state. The initial items are used as the starting point in the scan function. Additionally, startWith is utilized to emit the initial items.

For a demonstration, check out this StackBlitz example.

Answer №2

Starting off, it is not necessary to have subjects in order to propagate HTML events.

Angular actually utilizes an EventEmitter, which essentially functions as a subject, to disseminate changes throughout the application.

Thus, this

<button (click)="remove$.next(i)">remove</button>

Should be changed to this

<button (click)="removeItem(item, i)">remove</button>

Moving on, for the route data, you can easily create a subject using basic operators

routeData$ = this._route.data.pipe(pluck('ITEMS_KEY'), shareReplay(1)),

This results in cleaner code for your component :

routeData$ = this._route.data.pipe(pluck('ITEMS_KEY'), shareReplay(1)),

constructor(private readonly _route: ActivatedRoute) {}

addItem(item: any) {
  // ...
}

removeItem(item: any) {
  // ...
}

Lastly, it is crucial to determine how this impacts your data. What exactly should addItem and removeItem accomplish in the end? There are various options available, such as :

  • Making HTTP calls to your API
  • Updating your application state
  • Redirecting to the same route while modifying the data/route parameters, etc.

By implementing these changes, you can eliminate the need for subscriptions and let Angular handle the heavy lifting for you.

Moreover, by switching to the OnPush detection strategy, you can significantly enhance your application's performance!

Answer №3

Here's my perspective on the issue: Check out this link for more details.

In my opinion, using Subjects to simulate events may be unnecessary. Instead, creating new state for the items$ observable in two functions could provide a simpler solution.

Answer №4

After reviewing the answer provided by @BizzyBob, which I believe is the most suitable (addresses the question, uses rxjs, and is easy to understand), I would like to present my own take using the state management library effector.dev.

Explanation: In scenarios where external sources need to be integrated, the process tends to get complex once again. This is why I have moved away from using RxJS-based state managers, unfortunately.

Here is my implementation with effector: Demo

@Injectable()
export class ListViewEffectorService {
  public items$ = createStore<string[]>([]);
  public addItem = createEvent<string>();
  public removeItem = createEvent<number>();

  constructor(private readonly _route: ActivatedRoute, private ngZone: NgZone) {
    this.ngZone.run(() => {
      sample({
        clock: fromObservable<string[]>(
          this._route.data.pipe(map((items) => items[ITEMS_KEY] as string[]))
        ), // 1. when happened
        source: this.items$, // 2. take from here
        fn: (currentItems, newItems) => [...currentItems, ...newItems], // 3. convert
        target: this.items$, // 4. and save
      });

      sample({
        clock: this.addItem,
        source: this.items$,
        fn: (currentItems, newItem) => [...currentItems, newItem],
        target: this.items$,
      });

      sample({
        clock: this.removeItem,
        source: this.items$,
        fn: (currentItems, toRemove) =>
          currentItems.filter((_, idx) => idx !== toRemove),
        target: this.items$,
      });
    });
  }
}

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

Utilize Pipe for every instance of a variable in the Controller

In my controller, I have multiple requests (POST, GET etc.) where the path includes an id parameter that needs to be a number string. I want to validate this parameter once and have it apply to all instances. Here is the current code snippet: @Get(&apo ...

Using Typescript: invoking static functions within a constructor

This is an illustration of my class containing the relevant methods. class Example { constructor(info) { // calling validateInfo(info) } static validateInfo(info):void { // validation of info } I aim to invoke validateInfo ...

The "library" is encountering errors due to missing dependencies, specifically @angular/material/form-field

I've been working with a shared component library project that has been running smoothly for a while now. Recently, I decided to replace some of the custom components with Angular Material components. However, after adding NgMat to the library project ...

Do you have any suggestions for optimizing an Angular 15 reactive form that gets filled with data from an API?

Creating an Angular 15 Reactive Form with FormGroup As I delve into the documentation to construct a form with 4 inputs that are populated with data retrieved via GET requests and then updated upon submission through PUT requests, I find that it is functi ...

What could be causing my for loop to not function properly within the ngOnInit lifecycle hook?

I am attempting to create a nested loop structure in order to access an array that is inside an object within an array of objects, and then store this data into a new array. My issue arises as the first loop executes successfully but the second one does no ...

The component fails to load on the designated router outlet

I have updated my question to provide more clarity. I am working with 2 routing files, admin-layout.routing.ts and parameter.routing.ts. In admin-layout.routing.ts, there is a path for property.component.ts with a child called parameter.component.ts. Insid ...

Issue encountered when trying to use Array.sort() method to sort an array of objects

I'm facing an issue sorting an array of objects by a name property present on each object. When utilizing the sort() method with the given code snippet, I encounter the following error: ERROR ReferenceError: b is not defined This is my code block: m ...

How to properly handle sending an empty post request in Angular 2

My current issue revolves around attempting to send data from my application to the server using a POST request, however, the server seems to be receiving empty POST data. This is the function responsible for sending the data: private headers = new Heade ...

Is there a way to incorporate a button with row span in ag-grid within an Angular project?

In my project, I have incorporated ag-grid. I successfully implemented a cell renderer to display a button in the "action" column. The button appears on each row and functions properly when clicked. Now, for my second task, I am looking to include a butto ...

Issues with try_files directive in nginx.conf file

I have a situation where I am working on an angular 2 app, using nginx and docker. However, whenever I reload a page with /site in the URL, it gives me a 404 error. Here is my current server block configuration: server { listen 0.0.0.0:80; listen [::]:80; ...

filtering an array based on a specific property will result in the original array remaining

Working on filtering an array of objects based on a certain property using the following code snippet: if (payment == Payment.CREDIT_CARD) { this.currenies.filter((currency: Currency) => currency.isFromEurope === true); console.log(this.currencies) ...

Encountering errors in Typescript build due to issues in the node_modules directory

While running a typescript build, I encountered errors in the node_modules folder. Despite having it listed in the exclude section of my tsconfig.json file, the errors persist. What's puzzling is that another project with identical gulpfile.js, tsconf ...

Sending the value of "username" between two components within Angular 2

I have a good understanding of nesting child components within parent components in Angular 2, but I'm a bit unclear on how to pass a single value from one component to another. In my scenario, I need to pass a username from a login component to a cha ...

Updating a property value within a JSON object: Adjusting attributes in a JSON data format

How can I modify a specific key's value in a JSON array like the following example: input = [{"201708":10,"201709": 12, "metric":"attritionManaged"},{"201708":10,"201709": 12, "metric":"attritionUnManaged"},{"201708":10,"201709": 12, "metric":"EHC"}] ...

Taking advantage of Input decorator to access several properties in Angular 2

I am currently working on a component that is designed to receive two inputs through its selector. However, I would like to make it flexible enough to accept any number of inputs from various components. Initially, I tried using a single @Input() decorator ...

Exploring subclasses in TypeScript

When working with TypeScript and defining an interface like the one below: export interface IMyInterface { category: "Primary" | "Secondary" | "Tertiary", } Can we access the specific "sub types" of the category, such as ...

Where can I find the Cypress.json file for Angular integration with Cypress using Cucumber?

We are currently transitioning from Protractor to Cypress utilizing Cucumber with the help of cypress-cucumber-preprocessor. While searching for Angular documentation on this setup, including resources like , all references lead to an automatically generat ...

implementation of a grid tile in an Angular component

Here's what I am attempting to accomplish: <md-grid-list cols="3"> <product *ngFor="let product of products" [product]="product"></product> </md-grid-list> The product component includes a md-grid-tile. The issue: The ...

Convert all existing objects to strings

I have a type that consists of properties with different data types type ExampleType = { one: string two: boolean three: 'A' | 'Union' } Is there an easier way to define the same type but with all properties as strings? type Exam ...

What is the correct way to declare a variable with a generic type parameter?

Exploring the following code snippet that showcases a React component being defined with a type argument named TRow: function DataTable<TRow> ({ rows: TRow[] }) { return ( ) } Prior to this implementation, ES6 was utilized and components were c ...