Skip to content

Notes on NGRX patterns and handling observable based data changes in the Angular frontend

danoswaltCL edited this page Nov 10, 2022 · 2 revisions

The following is pulled from a PR comment that got out of hand because we have no real documentation on how NGRX works in our app. I think there is also some good guidance here for how to use event-driven / observable / rxjs patterns to reactively manage data changes versus sprinkling little boolean flags and redundant conditional checks all over our components to manage data changes after-the-fact.


@Yagnik56 thanks for incorporating ngrx, I think there are still some things that can make this even smoother but this is pretty close to what I was suggesting, it is mostly organization + a couple tricks I want to show. I want to also add that I appreciate the thoroughness of checking for actual data changes, I probably would have been lazy and just always shown the speedbump no matter if the data had changed or not 😄. So this is all a bonus if we can get this working in the simplest way.

I'll try to write out my suggestions here, this may get a little long but nobody has ever really documented this for our app, so I will turn this into the missing documention. Feel free to reach out.


Suggestion 1: Organization:

Your ngrx files will work the way they are, but it's not consistent with how the rest of the app is laid out for state change patterns. If you take a look in app -> core you will see a bunch of feature "services" with structures usually like this:

core
  |_ feature
       |_ feature.service.ts
       |_ feature.data.service.ts <-- FYI data.service files are used for making http calls, not relevant for this
       |_ feature.modules.ts
       |_ store <-- ngrx stuff goes here
           |_ feature.actions.ts
           |_ feature.effects.ts
           |_ feature.reducer.ts
           |_ feature.selector.ts
           |_ feature.model.ts

The component interacts with a feature.service file that handles the actual ngrx business logic. Ideally, we don't want store$ or any other ngrx objects pulled into a component's dependencies, that should be the work of the service. That keeps all implementation details out of the component and in service layer.

## trigger a data update event from component or service ##
feature.component -> feature.service -> service dispatches `ngrx` feature.actions ->  feature.reducer updates `store`

## get notified about data value changes ##
`store` -> feature.selectors ->  feature.service -> feature.component

So we need to make sure the ngrx stuff you've created is located in right spot. You could either create a new feature service under core and scaffold out some of files like this, or you could add your stuff to an existing service.

For simplicity, I recommend using core -> experiments to locate your new ngrx stuff. It would probably have been smart for us to have an "experimentDesignStepper" feature service, but we don't. Potentially you can create that! (please name something other than data_changed_flag, as that could mean anything).

The rest of my suggestions will show adding to experiments feature ngrx services. If you want a new experimentDesignStepper service, it would be similar.

  1. experiments.model: update the ExperimentState object type to include this flag, please with a name that is explicit, not just data_changed. If you look at other boolean flags on this object, the pattern is like hasExperimentDesignStepperDataChanged or something like that.

  2. experiments.actions: put your actions here.

  3. experiments.reducer: put your reducer functions here.

  4. experiments.selectors: create a selector for this flag.

  5. experiment.service: the service file is where you want to abstract handling component <-> ngrx interactions. I'd have a method for handling dispatching setting it to true, a method for handling dispatching resetting it to false, and a method to get the last emitted value from the hasExperimentDesignStepperDataChanged$ selector observable.

(For an example of simple boolean flag in ngrx, take a look at how the isAliasTableEditMode boolean value on the Experiment State is used.)

Now, with all of that out of the way, you will call the experiments.service methods to handle hasExperimentDesignStepperDataChanged flag dispatches and updates, so that the component doesn't have to do any of the implementation or even know that ngrx exists.


Suggestion 2: Some tricks we can use to stop tracking and checking data changes manually and start using events and observables:

It would be much better if we could replace this logic for showing the speedbump:

if (this.flag || this.participantsForm.dirty || this.participantsForm2.dirty || this.isRowRemoved)

with this:

if (this.experimentService.getHasExperimentDesignStepperDataChanged()) {

Which will get the boolean from the data store, because if we are always setting that flag to true when form is dirty or a row is removed, it will always be the source of truth. And this.flag is not needed at all, we only need to read this value for one purpose, there's no point in storing it on the component if we can avoid it.

  • How to get rid of checking this.isRowRemoved

Replace this.isRowRemoved = true with the service method to dispatch setting the flag to true. Then isRowRemoved can go away, we don't care if it's false!

  • How to get rid of checking for form.dirty:

One angular trick we should be able to do here is to subscribe to the statusChanges observable on any form object during the ngOnInit lifecycle hook:

this.form.statusChanges
.pipe(
  distinctUntilChanged() // this may be needed to ensure it only fires once when the value chances
)
.subscribe(
    (status) => {
      console.log(status)
      if (status === 'dirty') {
        // call the method to dispatch data change flag to true
      }
    }
  );

This code will get redundant, so a service method can be created:

component.ts

this.form.statusChanges
.pipe(
  distinctUntilChanged() // this may be needed to ensure it only fires once when the value chances
)
.subscribe(
    (status) => {
      this.experimentsService.updateFormStatus(status);
    }
  );

experiments.service.ts

updateFormStatus(status: string) {
  if (status === 'dirty') {
    // call the method to dispatch data change flag to true
  }
}

In this manner, if any form is dirty, it will set that flag to true automatically without any extra code.

If this works, then we're always dispatching an action to set hasExperimentDesignStepperDataChanged to true when the form is dirty or when a row is deleted. So then we can just check the hasExperimentDesignStepperDataChanged via the service getter instead of managing a lot of extra flags.

Clone this wiki locally