Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(store): implements ngxsOnChanges #1389

Merged
merged 11 commits into from
Oct 25, 2019
Merged

feat(store): implements ngxsOnChanges #1389

merged 11 commits into from
Oct 25, 2019

Conversation

splincode
Copy link
Member

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

[ ] Bugfix
[x] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] Other... Please describe:

What is the current behavior?

Issue Number: #749

What is the new behavior?

The method receives a NgxsSimpleChanges object of current and previous property values. This is convenient if we want to dispatch any additional actions when any fields have changed. Called whenever state change.

The main motivation is that this is a convenient hook in order to save states on the server, if we need it. We also have the opportunity to call any third-party services directly from the method, without the need to subscribe to changes to the state.

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Copy link
Member

@markwhitfeld markwhitfeld left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting!
Could you describe the API and its usages a bit in the description... this could become docs later ;-)

@splincode
Copy link
Member Author

splincode commented Oct 20, 2019

@markwhitfeld This API unifies the life cycle, thereby making our states universal and similar to Angular components.

https://angular.io/guide/lifecycle-hooks

Also there are many reasons to add this functionality, so it is necessary in the following use cases:

  • A convenient way to track state changes

Before

@State({ })
class MyState {
}
@Component({ })
class MyComponent {
  constructor(store: Store) {
    store.select(MyState).subscribe((newState) => {
       console.log('state is changed', newState);
    })
  }
}

One of the problems is, if we do not use the logging plugin (the official example does not use it), then we do not know what the previous state was before our state changed. It's great to have such an opportunity out of the box.

After

@State({ })
class MyState {
  ngxsOnChanges({  previousValue,  currentValue }: NgxsSimpleChange) {
     console.log('state is changed', previousValue, currentValue);
  }
}

Very comfortably!

Note: Why NgxsSimpleChange, again for the sake of versatility.

https://angular.io/api/core/SimpleChange

  • Convenient to synchronize with the server

In my work when using state, I needed to save state on the server every time the client changed it.

Before

@State({ })
class MyState {
}
@Component({ })
class MyComponent {
  constructor(store: Store, api: ApiService) {
    store.select(MyState).subscribe(async (newState) => {
       await api.saveToRedis(newState); 
    })
  }
}

After

@State({ })
class MyState {
  constructor(api: ApiService) {}

  async ngxsOnChanges({ currentValue: newState }: NgxsSimpleChange) {
     await api.saveToRedis(newState);
  }
}
  • You can write your own custom logger without another plugins
@State({ })
class MyState {
  ngxsOnChanges({ previousValue,  currentValue }: NgxsSimpleChange) {
     console.log('prev state', previousValue); // you can add color
     console.log('next state', currentValue);   // you can add color
  }
}

Or

@State({ })
class MyState {
  constructor(myLogger: MyLoggerService) {}
  ngxsOnChanges(change: NgxsSimpleChange) {
     myLogger.prettyDiffChanges(change);
  }
}

This will allow users to independently control logging without complaining about bugs of the logging plugin (#561, #276, #213).


Why ngxsOnChanges(change: NgxsSimpleChange) {} instead ngxsOnChanges(change: NgxsSimpleChange, ctx: StateContext<any>) {}?

One of the main reasons is that users should not manipulate state in this hook! Therefore, we should not motivate them to do this!

If that were so

@State({ })
class MyState {
  ngxsOnChanges(_: NgxsSimpleChange, ctx: StateContext<any>) {
     ctx.patchState({  onChange: true }); // Error: Maximum call stack size exceeded error
  }
}

Because the hook is called before setting the state, but if we allow the user to do in this method, then this will lead to various kinds of errors.

However, now users can make such mistakes.

@State({
   name: 'counter',
   defaults: 0
})
class MyState {
  constructor(private store: Store) {}

  ngxsOnChanges() {
     this.store.dispatch({ type: 'increment' }); // Error: Maximum call stack size exceeded error
  }

   @Action({ type: 'increment' })
    increment({ setState }: StateContext<number>) {
      setState((state: number) => ++state);
    }
}

In the future I will add verification so that users do not make such errors.

It will be very similar to this ExpressionChangedAfterItHasBeenCheckedError in Angular (problem with recursive change detection)

https://medium.com/better-programming/expressionchangedafterithasbeencheckederror-in-angular-what-why-and-how-to-fix-it-c6bdc0b22787

I will add a check, which in developer mode will check and throw an exception when users try to change the state directly in this hook (NgxsStateChangedAfterItHasBeenCheckedError)

Copy link
Member

@markwhitfeld markwhitfeld left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely done!
I really like the potential for this feature. It can simplify a great deal.
It also has a great harmony with the existing Angular lifecycle.
@arturovt What do you think?

packages/store/src/internal/state-context-factory.ts Outdated Show resolved Hide resolved
packages/store/src/internal/state-context-factory.ts Outdated Show resolved Hide resolved
packages/store/src/symbols.ts Show resolved Hide resolved
@markwhitfeld
Copy link
Member

Once @arturovt is happy then we can merge and add to the 3.6 release.

@markwhitfeld
Copy link
Member

cc @joaqcid I would like to hear your thoughts here too...

@joaqcid
Copy link
Contributor

joaqcid commented Oct 22, 2019

cc @joaqcid I would like to hear your thoughts here too...

i like this hook! i think it can be very handy for the scenarios described

Copy link
Member

@markwhitfeld markwhitfeld left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better with those changes.
Just some comments below to discuss.

packages/store/src/symbols.ts Outdated Show resolved Hide resolved
packages/store/src/symbols.ts Outdated Show resolved Hide resolved
packages/store/src/symbols.ts Outdated Show resolved Hide resolved
@markwhitfeld
Copy link
Member

@arturovt Would you like to review?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants