Getting error in react-redux reducer unexpected - reactjs

I thought I had safely created a new state and returned with the following code inside my Redux reducer. Basically, I need to change one record inside my state.data array and return the new state. My expectation was that since newState is completely new that I was good.
After installing redux-immutable-state-invariant, I'm seeing that I still have a problem (code and error below).
let newState = Object.assign({}, state);
let newData = [];
newState.data.map(function(rec){
if (rec.id === sessionIdToUpdate) {
rec.interestLevel = newInterestLevel;
newData.push(rec);
} else {
newData.push(rec);
}
});
newState.data = newData;
return newState;
and the error...
A state mutation was detected inside a dispatch, in the path: `sessions.data.0.interestLevel`
If I comment out the line
rec.interestLevel = newInterestLevel;
the error goes away, but then so does the purpose of this reducer.

That's because Object.assign({}, state) creates a shallow copy, and data and all its rec objects are shared with the previous state object. If you assign to one of them, you mutate the old state.
Instead of mutating the rec, you can specify the new state declaratively with something like this:
const newState = {
...state,
data: state.data.map((rec) =>
rec.id === sessionIdToUpdate ? {...rec, interestLevel: newInterestLevel} : rec
)
};

Related

Redux reducer doesn't cause re-render after mapping to new array or using immer

In my redux reducer I map over state and copy internal items, then return the list, the list should be a new reference so the reducer should cause a re-render on change, but it doesn't.
The code below does not cause a re-render.
const initialState: Group[] = [];
export default function activeGroups(state = initialState, action: AnyAction) {
switch (action.type) {
case 'groups/createPod/fulfilled': {
// append the new pod into the active group that matches the groupId
const { pod, groupId } = action.payload;
const newState = state.map((group) => { // Map to new list
if (group.id === groupId) {
return {
...group, // Not mutating original state, copying the state into new list
pods: [pod, ...group.pods],
};
}
return group;
});
return newState; // This does not cause a re-render, why?
}
I've tried produce from immer
case 'groups/createPod/fulfilled': {
// append the new pod into the active group that matches the groupId
const nextState = produce(state, (draft) => {
const group = draft.find((e) => e.id === action.payload.groupId);
if (group) {
group.pods.unshift(action.payload.pod);
}
});
return JSON.parse(JSON.stringify(nextState)); // Even tried this
}
Figured it out. Redux is re-rendering as normal. The problem was unrelated to Redux.
It was because the object I was trying to update was being passed as a navigation param in react native, and this does not update with state changes (the reference is to the object passed through navigation, not to the Redux state).
Using useSelector() solves it.

Redux: A state mutation was detected inside a dispatch

I realise there are probably a million and one different causes for this error but I am struggling to find the cause of mine. I am relatively new to Redux and state handling in the Reducer and have learned from a few examples online to come up with the code sample below which is called within the Reducer:
const updateTripsWhereVenueDeleted = (state, action) => {
debugger;
const deletedVenueId = action.venue.Id;
const originalTrips = [...state];
const newTrips = originalTrips.map((trip) => {
if (trip.VenueId === deletedVenueId) {
trip.VenueId = 0;
trip.VenuePegId = 0;
}
return trip;
});
debugger;
return [...newTrips];
};
I have state which is an array of Trips:
And Action contains a 'Venue' record.
Basically, the Venue being passed was deleted and I want to update the relevant fields on all Trips that reference the deleted Venue.
When I run the above code in the Reducer things seem to go in the right direction until the browser crashes with the following error:
Unhandled Rejection (Invariant Violation): A state mutation was
detected inside a dispatch, in the path: trips.0.VenueId. Take a
look at the reducer(s) handling the action
{"type":"UPDATE_TRIPS_VENUE_DELETED","venue": {.... the object}
UPDATE_TRIPS_VENUE_DELETED is the action.type that calls my method above.
Is there an obvious mis-use of handling (spread) arrays in a state or something. I feel this should be an easy thing to do but nothing I have tried has so far worked correctly.
Spreading an object or array ([...state] here) does not break nested references. So you are mutating state by not making a copy of the nested object within your map -- trip.VenueId = 0;.
This leads to another observation, map returns a new array, so this negates the need to use originalTrips altogether. It is just as safe to do state.map(). The final spread [...newTrips] is definitely unnecessary as well.
To fix your mutation create a clone of the objects to be updated:
const updateTripsWhereVenueDeleted = (state, action) => {
const deletedVenueId = action.venue.Id;
const newTrips = state.map((trip) => {
if (trip.VenueId === deletedVenueId) {
// Copy other trip values into NEW object and return it
return { ...trip, VenueId: 0, VenuePegId: 0 };
}
// If not matched above, just return the old trip
return trip;
});
return newTrips;
};
Spread operator ... only does shallow copy. If the array is nested or multi-dimensional, it won't work.
There are plenty of way we can do this, below are some of them
1] Deep copy using JSON
const originalTrips = JSON.parse(JSON.stringify(state));;
2]
const newTrips = originalTrips.map((trip) => {
if (newTrip.VenueId === deletedVenueId) {
let newTrip = Object.assign({},trip);
newTrip.VenueId = 0;
newTrip.VenuePegId = 0;
return newTrip;
}
return trip;
});

React : reducer isn't updating my state when using Object.assign

I use a reducer to update this state :
const [playlist, dispatch] = useReducer(jspfReducer,new playlistModel());
It uses my playlistModel class, which is just a wrapper that adds some methods to manipulate easily my data - I need it.
I want to update the state, but avoid as much possible unecessary renders.
So when calling the reducer case UPDATE_JSPF_TRACK; I update only the matching track of the playlistModel track array.
function jspfReducer(state, action){
switch (action.type) {
case 'UPDATE_JSPF_TRACK':
const [index,jspf] = action.payload;
//update only that single track.
const newTracks = state.track.map(
(track, i) => i === index ? new trackModel(jspf) : track
);
const newState = Object.assign(
state,
{
track:newTracks
}
);
//my object value is correctly updated here :
console.log("NEW STATE",newState);
return newState;
break;
}
};
The value logged in the console is correctly updated.
But in my provider, the state update is not detected:
export function PlaylistProvider({children}){
const [playlist, dispatch] = useReducer(jspfReducer,new playlistModel());
//when state updates
useEffect(()=>{
console.log("PLAYLIST HAS BEEN UPDATED!!!",playlist);//does not fire
},[playlist])
What is wrong and how could I fix this ?
Thanks !
You'll need to create a new object for the state changes to be detected:
const newState = Object.assign(
{},
state,
{
track: newTracks,
},
);
or
const newState = { ...state, track: newTracks };
The way React works is it detects if the state objects have different referential equality, which basically means that if you only modify an object's attributes, it will still be considered the same object for the purposes of rendering. To trigger state change effect you'll need to create a new object.

React componentWillReceiveProps will receive props not triggering when redux state updates

I've got a store which holds actions, these actions are suppose to be looped through each time a new one is added.
I've got a component MessageListView which resides in a parent called MessageView. When a new action is added to my socketActions.queue array, the componentWillRecieveProps is suppose to trigger, but it doesn't.
Here is what my reducer looks like:
/* Reducer */
const initialState = {
queue: [{
type: 'alert',
action: 'alert',
completed: false, // Set to true or just delete this object when the action has been completed
data: {
message: "the data object dynamically changes based on the type"
}
}]
};
export default (state = initialState, action) => {
switch (action.type) {
case ADD_ACTION:
let queue = state.queue
// action.data['completed'] = false
queue.push(action.data)
console.log("Just updated queue", action.data)
return {
...state,
queue: queue,
latestAction: new Date()
}
My component is connected up to the store like this:
function mapStateToProps(state){
console.log(state, "State inside messagelistview.js")
return {
actionsQueue: state.socketActions.queue,
latestAction: state.socketActions.latestAction
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ completeAction }, dispatch);
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(MessageListView);
So when I dispatch a new ADD_ACTION action, my state updates & redux-logger prints out the new and old state which is telling me that they're the same?! . I don't know why it'd be doing this after I've changed the latestAction value & queue array. This is why the componentWillRecieveProps isn't working, but I can't figure out why the state is the same?!
Not 100% certain if this would solve anything, but I think you aren't properly copying over state.queue into the completely new variable queue.
I'd suggest doing something like:
let queue = state.queue.slice()
... and see if anything changes? Right now your queue variable is still the same as the state.queue
You don't change queue identity in reducer. Try this code:
case ADD_ACTION:
let queue = state.queue
// action.data['completed'] = false
queue.push(action.data)
console.log("Just updated queue", action.data)
return {
...state,
queue: [...queue], // <-- here we copy array to get new identity
latestAction: new Date()
}
You should always shallow copy changed objets. See http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments
A connected component does a shallow check (===) whether the state is updated and renders wrapped component only if that check fails. In your case, you're mutating your queue causing the equality the check to pass.
It will work if you change your reducer like this:
state.queue = state.queue.concat(action.data);
or with ES6 syntax:
state = { ...state, queue: [...state.queue, action.data] };
The problem is that you are mutating your state when you do this:
let queue = state.queue
// action.data['completed'] = false
queue.push(action.data)
When you mutate the existing state directly, Redux doesn't detect a difference in state and won't notify your components that the store has changed.
So instead you've the following options to create a new queue array:
case ADD_ACTION:
return {
...state,
queue: state.queue.concat(action.data),
latestAction: new Date()
}
or with ES6 sugar:
case ADD_ACTION:
return {
...state,
queue: [...state.queue, action.data]
latestAction: new Date()
}

How can I toggle property in reducer?

I have built a cart app with this reducer in reactjs/redux:
const initialState = {
items: [],
cartOpen: false,
total: 0
}
const Cart = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TO_CART':
let newstate = [...state, action.payload];
var newTotal = 0;
newstate.forEach(it => {
newTotal += it.item.price;
});
newstate.total = newTotal;
newstate.cartOpen =true
return newstate;
case 'TOGGLE_CART':
debugger;
return !state.cartOpen;
default:
return state
}
}
export default Cart;
I am trying to set the state for the cart ie open but when I check the logs the cart property is updated and not the cartOpen property?
Redux assumes that you never mutate the objects it gives to you in the
reducer. Every single time, you must return the new state object.
Even if you don't use a library like Immutable, you need to completely
avoid mutation.
case 'TOGGLE_CART':
return !state.cartOpen;
Doing ^^ this is mutating your state (corrupting your state object). When you don't guarantee immutability, Redux loses its predictability and efficiency.
To achieve immutable state, we can use vanilla Object.assign or its more elegant alternative object spread syntax.
case 'TOGGLE_CART':
return {
...state,
cartOpen: !state.cartOpen
}
Your reducer must always return the complete slice of the app's state for which it is responsible. For TOGGLE_CART, you are only returning the boolean value for openCart.
Instead, create a copy of the previous state object and only update the single property you want to change:
case 'TOGGLE_CART':
return Object.assign({}, state, {
cartOpen: !state.cartOpen
});

Resources