Issue
I’m working with a basic Angular 11 application which has authentication implemented (using AWS Cognito & Amplify). What I’m trying to do here is quite simple. I am using builtin AWS Amplify methods to do the authentication. I am using NGXS to store the auth related items like tokens, user info etc.
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { AuthStateModel, defaultAuthState } from './auth.model';
import {
LoginAction,
AuthenticationCheckAction,
LogoutAction,
} from './auth.actions';
import { Auth } from 'aws-amplify';
import { environment } from 'src/environments/environment';
import { Injectable } from '@angular/core';
import { updateItem } from '@ngxs/store/operators';
@State<AuthStateModel>({
name: 'authState',
defaults: defaultAuthState,
})
@Injectable()
export class AuthState {
cognitoClientId = environment.cognitoClientId;
cognitoCallbackURL = environment.cognitoCallbackURL;
cognitoDomainName = environment.cognitoDomainName;
loginurl = `https://${this.cognitoDomainName}/login?response_type=code&client_id=${this.cognitoClientId}&redirect_uri=${this.cognitoCallbackURL}`;
@Selector()
static getIsLoggedIn(state: AuthStateModel) {
console.log('from getIsLoggedIn selector =>', state);
return state.isLoggedIn;
}
@Action(LoginAction)
login() {
window.location.assign(this.loginurl);
}
@Action(AuthenticationCheckAction)
checkLogin({ getState, patchState }: StateContext<AuthStateModel>) {
const state = getState();
Auth.currentAuthenticatedUser()
.then((currentUser) => {
const isLoggedIn = true;
const username = currentUser?.attributes?.name;
patchState({
currentUser,
isLoggedIn,
username,
});
})
.catch((err) => console.log(err));
return;
}
@Action(LogoutAction)
logout({ patchState }: StateContext<AuthStateModel>) {
Auth.signOut()
.then((data) => {
console.log(data);
patchState(defaultAuthState);
})
.catch((err) => console.log(err));
return;
}
}
Everything works except when it hits the patchState method inside the checkLogin function, it throws the following error in the browser console.:-
TypeError: Cannot freeze
at Function.freeze (<anonymous>)
at deepFreeze (ngxs-store.js:1869)
at ngxs-store.js:1884
at Array.forEach (<anonymous>)
at deepFreeze (ngxs-store.js:1874)
at ngxs-store.js:1884
at Array.forEach (<anonymous>)
at deepFreeze (ngxs-store.js:1874)
at ngxs-store.js:1884
at Array.forEach (<anonymous>)
Curiously, the error is gone if I comment out the mention of currentUser
from the pathState() method. So it is the update of the object inside the state that is problematic.
The line referenced in this error of the ngxs-store.js file contains the following code. The line at 1869 is the Object.freeze(o) one at the beginning:-
const deepFreeze = (/**
* @param {?} o
* @return {?}
*/
(o) => {
Object.freeze(o);
/** @type {?} */
const oIsFunction = typeof o === 'function';
/** @type {?} */
const hasOwnProp = Object.prototype.hasOwnProperty;
Object.getOwnPropertyNames(o).forEach((/**
* @param {?} prop
* @return {?}
*/
function (prop) {
if (hasOwnProp.call(o, prop) &&
(oIsFunction ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments' : true) &&
o[prop] !== null &&
(typeof o[prop] === 'object' || typeof o[prop] === 'function') &&
!Object.isFrozen(o[prop])) {
deepFreeze(o[prop]);
}
}));
return o;
});
Not savvy enough to understand what’s going on here. This is a simple attempt to patch the state. The state model is as follows:-
export class AuthStateModel {
isLoggedIn: Boolean;
currentUser: Object;
username: String;
}
export const defaultAuthState: AuthStateModel = {
isLoggedIn: false,
currentUser: {},
username: '',
};
Is there anything I can do to get this to work properly?
Solution
Sometimes when I run into an issue like this, where I can’t change an object, I just clone it. I typically use lodash’s cloneDeep for this:
patchState({
currentUser: _.cloneDeep(currentUser),
isLoggedIn,
username,
});
Also, try to keep your state serializable. Not familiar with AWS Cognito & Amplify, but if the currentUser is an object that has methods and other members tied to it that is needed for later, then I wouldn’t store the currentUser object in state.