Ngrx : Cannot assign to read only property 'Property' of object '[Object]' - javascript

I'm using ngrx store.
In my state I have to items
export interface ISchedulesState {
schedulings: ISchedules;
actualTrips: ISchedule[];
}
Here are my interfaces
export interface ISchedules {
[key: string]: ISchedule[];
}
export interface ISchedule {
dest: number;
data: string
}
In reducer I update actualTrips
export const SchedulingReducers = (
state = initialSchedulingState,
action: SchedulesAction
): ISchedulesState => {
switch (action.type) {
case ESchedulesActions.GetSchedulesByDate: {
return {
...state
};
}
case ESchedulesActions.GetSchedulesByDateSuccess: {
return {
...state,
schedulings: action.payload
};
}
case ESchedulesActions.GetSchedulesByTime: {
let time = action.payload;
state.actualTrips = [...(state.schedulings[time] || [])]; // if not data return empty array
return state;
}
default:
return state;
}
};
But actually I get an error
ERROR TypeError: Cannot assign to read only property 'actualTrips' of object '[object Object]'

The basic principle of Redux pattern is immutability of state and its parts, because it let's us to detect changes just by object reference instead of comparing whole objects.
In your reducer, you cannot directly assign a property of state (state.actualTrips =), because change detector (and selectors) would not detect it as changed.
To modify state, you return a copy of the state with new modifications.
const time = action.payload;
return {
...state,
actualTrips: [...(state.schedulings[time] || [])]
}

If you want to change state.actualTrips = myNewValue is not allowed because there is a strict Setting. So one way is may to clonedeep and return the object, like newState = cloneOfState... I didn't test it. So I changed the setting in app.module for Store.
My Example: change the strictStateImmutability to false (full Docu here: https://ngrx.io/guide/store/configuration/runtime-checks )
StoreModule.forRoot(ROOT_REDUCERS_TOKEN, {
metaReducers,
runtimeChecks: {
// strictStateImmutability and strictActionImmutability are enabled by default
strictStateSerializability: true,
strictActionSerializability: true,
strictActionWithinNgZone: true,
strictActionTypeUniqueness: true,
// if you want to change complexe objects and that we have. We need to disable these settings
// change strictStateImmutability, strictActionImmutability
strictStateImmutability: false, // set this to false
strictActionImmutability: true,
},
}),

That error happened me when I changed the input values in the template. I was using Angular11 + NGRX11 so I understood I was changed a value from store, this was my fix:
Before:
this.store.dispatch(new Actions.LoginUser({ user: this.user }));
After:
const clone = {
user: Object.assign({}, this.user)
};
this.store.dispatch(new Actions.LoginUser(clone));

I found the answer at https://stackoverflow.com/a/58279719
Simply copy the state into a new object
const oldState = getState();
let state = JSON.parse(JSON.stringify(oldState)); // here
state.propertyToChange = "newValue";
patchState({
...state
});

Related

Typescript reducer's switch case typeguard doesn't work with object spread

I have a reducer that does different actions depending on the action.type, actions payload is different for certain actions.
export enum ActionType {
UpdateEntireState = "UPDATE_ENTIRE_STATE",
UpdateStateItem = "UPDATE_STATE_ITEM"
}
type TypeEditData = {
id: string;
name: string;
surname: string;
age: number;
};
export type State = TypeEditData[];
export type Action = UpdateEntireState | UpdateStateItem;
type UpdateEntireState = {
type: ActionType.UpdateEntireState;
payload: State;
};
type UpdateStateItem = {
type: ActionType.UpdateStateItem;
payload: { id: string; data: TypeEditData };
};
export function reducer(state: State, action: Action): State {
const { type, payload } = action;
switch (type) {
case ActionType.UpdateEntireState: {
return [...payload];
}
case ActionType.UpdateStateItem: {
const person = state.filter((item) => item.id === payload.id);
return [...state, person[0]];
}
default: {
throw Error("Wrong type of action!");
}
}
}
This code won't work, the errors will say that my action payload can be State or { id: string; data: TypeEditData }.
However, if I access the payload property inside switch case using dot notation like so
return [...action.payload];
There won't be any errors and the type guard will work fine.
How const { type, payload } = action; differs from action.type and action.payload in terms of types and why doesn't typeguard work with spread syntax?
TS version - 4.3.4
The issue is that you've defined payload before there was type information available on action, so it has the union type
State | {
id: string;
data: TypeEditData;
};
Define a local variable or simply use action.payload within each case statement and the compiler knows what type it has:
export function reducer(state: State, action: Action): State {
// const { type, payload } = action;
switch (action.type) {
case ActionType.UpdateEntireState: {
return [...action.payload];
}
case ActionType.UpdateStateItem: {
const person = state.filter((item) => item.id === action.payload.id);
return [...state, person[0]];
}
default: {
throw Error("Wrong type of action!");
}
}
}
Variable type is established explicitly at declaration (e.g. const a: string) or implicitly at initialization (e.g. a = 4). Subsequent typeguard constructs are not used to re-evaluate the type of the variable. On the contrary, since the type of the variable is already defined at that point, that type is used to validate whether the later construct is valid for the variable.
Action interface by default comes with type property.
export interface Action {
type: string;
}
If you can extend Action interface to add payload as array of objects then typescript wont throw you an error.
Something like this, in your reducer function you can use like this
interface CustomAction extends Action{
payload: Array<any>
}
export function reducer(state: State, action: CustomAction): State {

The entity passed to the `selectId` implementation returned undefined

i want to set token and refresh token in ngrx and save and use that for every request and when the page was reload not delete data in redux .
i implementation this state for this :
i dispatch data :
this.getUserInformation().toPromise().then(responset => {
if (response.success === true) {
this.store.dispatch(new SetUserInformation({
displayName: responset['result']['displayName'],
userInfo: responset.result.claims,
RefreshTokenStatus:false,
accessToken:response['result']['access_token'],
refreshToken:response['result']['refresh_token']
}))
}
});
in module i definde store :
StoreModule.forFeature('Information', reducer),
EffectsModule.forFeature([])
this is my reducer :
const initialState = adapter.getInitialState({
accessToken: null,
refreshToken: null,
RefreshTokenStatus: false,
userInfo: null,
displayName: null
})
export function reducer(state = initialState, action: TokenAction): TokenState {
switch (action.type) {
case TokenActionTypes.UserInformation:
return adapter.addOne(action.payload, state)
default:
return state;
}
}
and this is my model :
export interface TokenState {
accessToken: string;
refreshToken: string;
RefreshTokenStatus: boolean;
userInfo: string[];
displayName: string;
}
this is my Selector:
export interface State extends fromState.State, EntityState<TokenState> {
UserInfo: TokenState;
}
export const adapter: EntityAdapter<TokenState> = createEntityAdapter<TokenState>();
const getTokenFetureState = createFeatureSelector<TokenState>('Information');
export const getAccessToken=createSelector(
getTokenFetureState,
state => state.accessToken
)
this is action :
export class SetUserInformation implements Action {
readonly type = TokenActionTypes.UserInformation;
constructor(public payload: TokenState) {
}
}
export type TokenAction = SetUserInformation
now i have tow problem :
this.store.pipe(select(fromTokenSelect.getAccessToken)).subscribe(data=>{
console.log(data)
})
A: when i want to get token for use that it return null and show me this error :
#ngrx/entity: The entity passed to the selectId implementation returned undefined. You should probably provide your own selectId implementation. The entity that was passed: Object The selectId implementation: (instance) => instance.id
B: when reload the page it delete data from ngrx.
how can i solve this problem ?
You are using ngrx/entity for non-entity data. Entity is designed to work with arrays of data, where each element has some kind of unique identifier. That is why it is throwing the error: you're feeding it non-entity-compatible data. You need to write a simple reducer which simply sets the state, not one which tries to add data to a non-existent entity.
As for page reload, NgRx will always reset the data, that is the expected behavior. You need to implement localStorage saving if you want to persist data over time.

Testing Actions, Reducers, & Contexts in React

I've built multiple React functional components using Hooks and Context. Everything works fine. Now I need to write tests for everything. I'm confused about how to move forward with some of them so wanted to reach out to the community.
Actions
Here's a sampling from one of my Actions files:
export const ADD_VEHICLE: 'ADD_VEHICLE' = 'ADD_VEHICLE';
export const UPDATE_VEHICLE: 'UPDATE_VEHICLE' = 'UPDATE_VEHICLE';
type AddVehicleAction = {type: typeof ADD_VEHICLE, isDirty: boolean};
type UpdateVehicleAction = {type: typeof UPDATE_VEHICLE, id: number, propName: string, payload: string | number};
export type VehiclesActions =
| AddVehicleAction
| UpdateVehicleAction;
How am I supposed to test this Actions file? I don't mean in conjunction with anything else, I mean it and only it?
From the comments, it appears I have agreement that there's nothing to test DIRECTLY in this file.
Reducers
Each of my Reducers files is directly connected to and supports a specific Context. Here's a sampling of one of my Reducers files:
import type { VehiclesState } from '../VehiclesContext';
import type { VehiclesActions } from '../actions/Vehicles';
import type { Vehicle } from '../SharedTypes';
import { ADD_VEHICLE,
UPDATE_VEHICLE
} from '../actions/Vehicles';
export const vehiclesReducer = (state: VehiclesState, action: VehiclesActions) => {
switch (action.type) {
case ADD_VEHICLE: {
const length = state.vehicles.length;
const newId = (length === 0) ? 0 : state.vehicles[length - 1].id + 1;
const newVehicle = {
id: newId,
vin: '',
license: ''
};
return {
...state,
vehicles: [...state.vehicles, newVehicle],
isDirty: action.isDirty
};
}
case UPDATE_VEHICLE: {
return {
...state,
vehicles: state.vehicles.map((vehicle: Vehicle) => {
if (vehicle.id === action.id) {
return {
...vehicle,
[action.propName]: action.payload
};
} else {
return vehicle;
}
}),
isDirty: true
};
}
If you wanted to build tests for JUST this Reducers file, what approach would you use? My thought was to render the DOM like this:
function CustomComponent() {
const vehiclesState = useVehiclesState();
const { isDirty,
companyId,
vehicles
} = vehiclesState;
const dispatch = useVehiclesDispatch();
return null;
}
function renderDom() {
return {
...render(
<VehiclesProvider>
<CustomComponent />
</VehiclesProvider>
)
};
}
While this code above does run, I now have the problem that both vehiclesState and dispatch are not accessible within my test code so I'm trying to figure out how to "surface" those within each describe / it construct. Any suggestions would be appreciated.
Contexts
My Contexts follow the same pattern outlined by Kent C. Dodds: https://kentcdodds.com/blog/how-to-use-react-context-effectively - in that the StateContext & DispatchContext are separated, and there's a default state. Given this code pattern and given that I'll already have a separate test file for the Context's Reducers, what specifically could one test for ONLY for the Context?
Same as my comment, I really think you should read redux docs for writing tests so that you get a general idea of what to do.
But since you already have a reducer, you want to write your test case to follow this pattern
you will have at least 1 test per action
each test will have a a "previous state", which will be altered
you will call your reducer, passing the action and the previous state
you will assert your new state is the same as expected
Here's a code example:
it('adds a new car when there are no cars yet', () => {
// you want to put here values that WILL change, so that you don't risk
// a false positive in your unit test
const previousState = {
vehicles: [],
isDirty: false,
};
const state = reducer(previousState, { type: ADD_VEHICLE });
expect(state).toEqual({
vehicles: [{
id: 1,
vin: '',
license: '',
}],
isDirty: true,
});
});
it('adds a new car when there are existing cars already, () => {
// ...
});
I'd also recommend to use action creators rather than directly creating action objects, since it's more readable:
// actions.js
export const addVehicle = () => ({
type: ADD_VEHICLE
})
// reducer.test.js
it('adds a new car when there are no cars yet', () => {
//...
const state = reducer(previousState, actions.addVehicle());

Add custom flow type for object method

I would like refacto' my method by adding flow type. My actual code :
const apiActions = {
registerThroughApi: async (
state: Object,
action: Function,
formData: Object
): Object => {
return state;
}
};
Is it possible to add flow type ?
type Form = {
state: Object,
action: Function,
formData: Object
}
const apiActions = {
registerThroughApi: async (state, action, formData)<Form>: Object => {
return state;
}
};
But this code don't work :D
Anyone know if it's possible ?
Thank you community !
not sure I'm understanding exactly, but what about something like this
// #flow
const apiActions = {
registerThroughApi: async (state: Object, action: Function, formData: Object): Object => {
return state;
}
};

Flow - How to setState with a value I get from a function?

In the getEvaluation function I get the number 1, I want to change the state with this value:
type Props = {
};
type State = {
id_evaluation: string,
};
class Evaluation extends Component < Props, State > {
state = {
id_evaluation: '1',
}
componentDidMount() {
const id_eval = getEvaluation();
this.setState({
id_evaluation: id_eval,
});
I checked now 'console.log(typeof(id_eval)), is string.
Flow generates this error:
Cannot call this.setState with object literal bound to
partialState because null or undefined [1] is incompatible with
string [2].
Try to convert id_eval into a string like this.
type Props = {
};
type State = {
id_evaluation: string,
};
class Evaluation extends Component < Props, State > {
state = {
id_evaluation: '1',
}
componentDidMount() {
const id_eval = getEvaluation();
this.setState({
id_evaluation: String(id_eval) || '',
});
How about this one:
state = {
id_evaluation: 1,
};
const id_eval = getEvaluation();
this.setState({
id_evaluation: id_eval,
});
The return value of the getEvaluation() function has a variety of cases, Number type, Null type or Undefined type, but your State limit accepts the String type, which is the error reported in the compilation phase, you need to convert the return value of getEvaluation(), for example using String(id_eval).

Categories