Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is frequent use of BehaviorSubject in Angular services a red flag?

I'm writing an application using Angular and find myself using this pattern constantly:

@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);
  }
}

A lot of services will have 3-5 or more such groups of public getters and private backing Subjects. It happens so much that the code feels very verbose and repetitive. So: a) is there a DRY way to do this, and b) am I misusing Observables here?

like image 485
Coderer Avatar asked Mar 03 '23 23:03

Coderer


1 Answers

I'm writing an application using Angular and find myself using this pattern constantly:

The pattern you've shown is very similar to a state store such as; Redux, NgRX or NGXS. The difference is that you've placed the store, selectors and reducers into a single class.

Having everything in one place has advantages, but if you have to rewrite a new store every time you start a new service, then that would explain why you say "It happens so much that the code feels very verbose and repetitive".

There is nothing wrong with this, and there are many blog posts on the Internet that try to write a Redux clone in as few lines of code as possible. My point is that people are doing exactly what you're doing all the time.

private readonly _widgets: BehaviorSubject<Widget[]> = new BehaviorSubject([]);

The above is the store of the state manager. It's an observable that contains the current state and emits changes to that state.

public get widgets() { return this._widgets.value; }

The above is the snapshot of the state manager. This allows you to do specific calculates on the store without having to subscribe, but just as with other state stores using snapshots can have race condition problems. You should also never access this directly from a template, because it will trigger the "Expression has changed after it was checked" error.

public readonly widgets$ = this._widgets.asObservable();

The above is a selector for the store. Stores will often have many selectors that allow different parts of the application to listen for store changes on specific topics.

public add(widget: Widget) {
   const old = this._widgets.value.slice();
   old.push(widget);
   this._widgets.next(old);
   // above can be rewritten as
   this._widgets.next([...this.widgets, widget]);
}

We don't have the above in state store libraries. The above is broken down into two parts the action and the reducer. The action often contains the payload (in your case a widget) and the reducer performs the work of modifying the store.

When we use actions and reducers it decouples the business logic of how the store should change from the issues of reading the current state, updating the state and saving the next state. While your example is very simple. In a large application having to subscribe, modify and emit the changes can become overhead boilerplate code when all you want to do is toggle a boolean flag.

A lot of services will have 3-5 or more such groups of public getters and private backing Subjects. It happens so much that the code feels very verbose and repetitive.

You're entering the realm of reinventing the wheel.

As I see it, you have two possible options. Invent your own state store framework that will feel more comfortable for you, or use an existing state store from one of the libraries I listed above. We can't tell you which path to take, but I've worked on many Angular projects and I can tell you there is no right answer.

What really makes source code feel less verbose and repetitive is highly opinionated. The very thing that made it less verbose might one day come back to haunt you as a design mistake, and repetitive source code is pain but one day you'll be thankful you can modify a single line of code without it impacting other areas of your source code.

a) is there a DRY way to do this, and

The only way to dry out the source code is to decouple the implementation of state management from the business logic. This is where we get into a discussion of what makes a good design pattern for a state store.

  • Do you use selectors?
  • Do you use actions?
  • Do you use reducers?

Where do you want these things to be (in their own files, or methods of a service?). How do you want to name them, and should you re-use them or create new ones for every edge case?

It's a lot of questions that are really personal choices.

I can rewrite your example using NGXS as an example, but this might not look dry to you because frameworks need to be complex to be useful. What I can tell you is that it's easier to read documentation for NGXS when you need to do something you haven't done before, then trying to invent it yourself and risk getting it wrong. That doesn't mean NGXS is always right, but at least you can complain it's not your fault :)

@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 misusing Observables here?

Absolutely not misusing observables. You have a good grasp of why a service should be stateless and reactive. I think you're just discovering the value of state stores on your own, and now you're looking for ways to make the use of them easier.

like image 104
Reactgular Avatar answered Mar 06 '23 22:03

Reactgular