Closed zygimantas closed 6 years ago
I have the same issue. The only workaround I found is to manually call ApplicationRef.tick() on each effect. For example:
@State<HomeStateModel>({
name: 'home',
defaults: new HomeStateModel(),
})
export class HomeState {
constructor(private service: MyHttpService, private ref: ApplicationRef) {
}
@Action(events.CreateCluster)
createCluster({ getState, setState }: StateContext<HomeStateModel>, { payload }: events.CreateCluster) {
return this.service.doSomething(payload).pipe(
tap(nodes => {
const state = getState();
setState({
...state,
nodes: nodes
});
this.ref.tick(); // FORCE A REFRESH HERE
}
),
catchError(err => {
this.ref.tick(); // FORCE A REFRESH HERE
return of("error message")
})
)
}
1) I don’t understand why you would make a subscribe like that vs just use a async pipe 2) this is introduced because of zone issue. We need to return select subscribe to the zone. I’ll make a patch shortly
this.panda
is used in button click handling/hammerjs/ngOnChanges methods in component.Gotcha, thanks for clarification.
should be fixed in 3.1.3
@amcdnl The error is still reproducible in 3.1.3.
This is the bad behavior that we have:
Everything works as expected with 3.0.1.
Can you make a stackblitz?
Just letting you know that my issue was solved with 3.1.3. Thanks!
@amcdnl You can find a repro here: https://github.com/ufoscout/WeWeb
Steps to reproduce:
npm install
npm start
Wrong behavior: the UI still displays "Ciao Mondo" (Italian) instead of "Hello World" (English). If you click anywhere on the screen, then "Hello World" is displayed.
If you downgrade from
"@ngxs/store": "^3.1.3"
to
"@ngxs/store": "3.0.1"
then the issue disappears and everything works as expected.
@ufoscout - I looked over your application, I believe what you are doing is a anti-pattern and it makes sense why its not working for you.
@Action(events.SetLanguage)
setToken({ getState, setState }: StateContext<CommonStateModel>, { payload }: events.SetLanguage) {
const state = getState();
setState({
...state,
language: payload.language,
});
this.translate.use(payload.language);
}
Here you are setting your translator in your state. This should not happen here. Only state modifications should happen in your state. I would recommend using action subscriptions to perform this.
Here is a example where I use a action subscription to log a user out and disconnect them from the websocket.
this._actions.pipe(ofActionDispatched(Logout)).subscribe(() => {
this.svc.disconnect();
this.router.navigate(['login']);
});
I would recommend you subscribing to that event in the action stream and then switching that there. Let me know if this works for you please.
@amcdnl yes, the bug is still present in 3.1.3. Please check my previous comment in which I provided a reproducer. There is, however, a difference between the behavior of 3.1.2 and 3.1.3; in fact:
@ufoscout - Check my updated answer.
I have a similar problem in my own project, I tried to debug using the Actions handler from the Actions import form ngxs/store, I am subscribing in the component and using the pipe ofActionDispatched() in that subscribe I console.log the value, the first time that the component is instantiated the UI gets updated, but for some reason after a navigate to other route and then return to the initial route and try again, the same ofActionDispatched subscription function console.logs the value but the UI wont update unless I do as @ufoscout says, to use tick() inside the subscription function. Side note I am subscribed to ofActionSuccessful over the same action and after navigating to other route and returning to the initial one, it Updates the console.log value as expected and the UI gets updated also, with no use of Tick() method!! I am using 3.1.3, have not tried downgrading.
@amcdnl I just tested with version 3.0.1 and it worked out.
I think I have the same issue with mat-dialog (@angular/material): https://ngxs-mat-dialog-issue.stackblitz.io
And It worked perfectly with version 3.0.1
Using dispatch button, the dialog is showed, but the buttons cancel and ok, don't work correctly ... The direct button shows same dialog without using @ngxs, and everything works...
@juracy - Your doing the same thing as him, you have code like:
@Action(ConfirmAsk)
ask(ctx: StateContext<ConfirmModelState>, action: ConfirmAsk) {
this.uiAsk(action.message)
ctx.setState({message: action.message})
}
uiAsk(message: string) {
this.dialog.open(ConfirmDialogComponent, {
width: '50%',
data: { message: message },
})
}
That function should NOT live in your state. You should use the action stream to subscribe and react. This is actually a good thing this is happening to weed out these anti-patterns.
Unless someone says otherwise, I'm going to close this because it seems like its working for the appropriate cases now.
@amcdnl I also tried with action handlers and I haven't got success anyway...
I've just updated my example with handler, and the same problem remains... https://ngxs-mat-dialog-issue.stackblitz.io
I could manage some workarounds:
@juracy I have just discovered what is troubling me with my code, the view doesn't gets updated as I said in my comment, 'cause i was trying to change the route with the Router from angular, directly from the app state (in my case after dispatching a Login action that was successful) I just replace it with the Navigate action, provided by '@ngxs/router-plugin' and problem solved!. keep in note I was trying to display statuses like 'pending' and 'error' that lives not in the state (as it could) but rather lives in the component, a gets updated by subscribing to it 'this.store.dispatch(new Login).subscribe(success => {}, error => {})'.
Conclusion, I think #376 was needed in some extend. As @amcdnl said
"Only state modifications should happen in your state"
maybe we should update the Docs to be a more cleare aboute what is permited to do inside the app state action function. For last, point to this ((https://github.com/ngxs/ngxs-examples)) example at the README file in the @ngxs/store Repo, because it help me allot to understand what was happening.
onSubmit() {
if (this.loginForm.enable) {
if (this.loginForm.valid) {
this.pending = true;
this.subscription = this.store.dispatch(new fromAuthActions.Login( this.loginForm.value )).subscribe(
() => { this.pending = false; },
error => { this.pending = false; this.loginForm.markAsPristine(); this.error = error.error; }
);
}
}
}
<div class="errors" *ngIf="loginForm.pristine && error">
Ocurrio un error
</div>
<div class="loading" *ngIf="pending">LOADING...</div>
@Action(fromAuthAction.Login)
login(ctx: StateContext<AuthStateModel>, { payload }: fromAuthAction.Login) {
return this.authService.login(payload).pipe(
tap(user => {
ctx.patchState({ user, isLoggedin: true });
this.router.navigate(['/']); <=== BAD
})
);
}
@Action(fromAuthAction.Login)
login(ctx: StateContext<AuthStateModel>, { payload }: fromAuthAction.Login) {
return this.authService.login(payload).pipe(
tap(user => {
ctx.patchState({ user, isLoggedin: true });
ctx.dispatch(new Navigate(['/'])); <=== GOOD
})
);
}
@jhonland like I said, my last example was updated in https://stackblitz.com/edit/ngxs-mat-dialog-issue and I took off all side effects in the state class, even though, the problem remains ... I'm using o action stream now!
And in your example about router, nowadays it's working using ngZone.run like you can see in @ngxs/router-plugin: https://github.com/ngxs/store/blob/01c499f1e33a40ffebbf66ffbb46fcadb6b17d9b/packages/router-plugin/src/router.state.ts#L55
I don't believe that is the best approach anyway...
@juracy - I looked at your example and it seems to work to me? What am i missing?
Also, is this your real use case? It feels a bit odd to put a confirm state in a global state container like that.
@amcdnl sorry, steps: 1 - Click on dispatch button 2 - Click on Cancel action button in dialog
Expected behavior: log a message in console and close dialog
@amcdnl Moving the language selection from the state, as you suggested, solved that particular the issue; nevertheless, there are still use cases that work fine in 3.0.1 but not in 3.1.3. For example, I have a spinner image to be displayed while certain actions are running. in 3.0.1 this works as expected:
@Action(events.Login)
login(ctx: StateContext<AuthStateModel>, {payload}: events.Login) {
this.spinner.show();
return this.loginService.login(payload).pipe(
map(response => {
this.spinner.hide();
ctx.dispatch(new events.SetAuthData(response));
}),
catchError(err => {
this.spinner.hide();
return throwError(err);
})
);
}
but in 3.1.3 the spinner is not shown. Moving the spinner from the state to somewhere else is not easy in this case. In fact, I cannot create a global interceptor since I need it only for some actions and create a special subscription to the Action Dispatched and Action Completed for each interested action seems really overcomplex for this simple task. Is there any technical reason to not re-enable the same behavior of 3.0.1?
@ufoscout my two cents: 1 - Move the spinner interaction to a handler (action stream), that's the best practice 2 - Even though, run interaction with UI within ngZone (at least, it's my current workaround)
@ufoscout - The state is not supposed to interact w/ the UI. Hiding the spinner/etc is UI related and since we run state outside of zone it will not fire. Those should be action handlers or driven by state in the UI.
@ufoscout You should start your spinner from inside your component before dispatching your action, then you should subscribe to that action dispatch an when subscribed its executed (after your action call your login API and return) you tell your spinner to stop. Something like this:
onSubmit() {
if (this.loginForm.enable) {
if (this.loginForm.valid) {
this.pending = true;
this.subscription = this.store.dispatch(new fromAuthActions.Login( this.loginForm.value )).subscribe(
() => { this.pending = false; },
error => { this.pending = false; this.loginForm.markAsPristine(); this.error = error.error; }
);
}
}
}
where the this.pending would be your spinner methods to start and to stop
@amcdnl I have took off UI code from state and put it in an action handler, but I still need to use ngZone, is that correct?
Did you see my example? https://stackblitz.com/edit/ngxs-mat-dialog-issue
Steps: 1 - Click on dispatch button 2 - Click on cancel button (in dialog)
Expected:
@amcdnl I can confirm that there is a bug if you use the action stream. It is because the action stream is not returning in the angular zone. I will submit a fix today.
@juracy See your sample with a temp workaround to get it working: https://stackblitz.com/edit/ngxs-mat-dialog-issue-eo2aqj?file=src/app/confirm/confirm-handler.ts
Also, see a sample repro here of the issue and potential workarounds until it is fixed: https://stackblitz.com/edit/ngxs-actionstream-bug-repro?file=app/app.component.ts
@markwhitfeld thanks! I had already been using ngZone! But I thought it's wrong or at least weird!
Confirmed fixed with latest dev package: https://stackblitz.com/edit/ngxs-mat-dialog-issue-fixed?file=src/app/confirm/confirm-handler.ts and https://stackblitz.com/edit/ngxs-actionstream-bug-repro-fixed?file=app/app.component.ts
just want to let you know ngxs 3.14 still has this bug in close button like @juracy bug,after update to 3.14 NGXS/store@dev the bug is fix
@jeffkenz89 This is not a bug, when you want to update the UI from the state code use the following
this.ngZone.run(() => {
this.dialog.open(ErrorDialogComponent, {
width: '250px',
data: payload.error.message,
disableClose: true
});
});
Versions
Repro steps
@Select(ZooState.panda) public panda$!: Observable<Panda>;
this.panda$.subscribe(a => this.panda = a);
Observed behavior
When
this.panda
is changed, change detection does not happen in production environment and UI is not updated (*ngIf="panda").Desired behavior
Change detection should happen.
Mention any other details that might be useful (optional)
It works as expected in 3.0.1 - all environments, 3.1.1/3.1.2 - development environment. There is NO ChangeDetectionStrategy.OnPush set on component.