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;
});
Related
For readability im going to strip out a lot of functionality in my examples. However, essentially I have a useEffect (shown below) that has a dependency that tracks the state.cards array of card objects. My assumption was that if that state.cards property changes then the useEffect should trigger. However, that's not exactly proving to be the case.
Below are two solutions that are being used. I want to use the first one since it's in constant time. The second, while fine, is linear. What confuses me is why the second option triggers the dependency while the first does not. Both are return a clone of correctly modified state.
This does not trigger the useEffect dependency state.cards.
const newArr = { ...state };
const matchingCard = newArr.cards[action.payload]; <-- payload = number
matchingCard.correct += 1;
matchingCard.lastPass = true;
return newArr;
This does trigger the useEffect dependency state.cards.
const newArr = { ...state };
const cards = newArr.cards.map((card) => {
if (card.id === action.payload.id) {
card.correct += 1;
card.lastPass = true;
}
return card;
});
return { ...newArr, cards };
useEffect
useEffect(() => {
const passedCards = state.cards.filter((card) => {
return card.lastPass;
});
setLearnedCards(passedCards);
const calculatePercent = () => {
return (learnedCards.length / state.cards.length) * 100;
};
dispatch({ type: 'SET_PERCENT_COMPLETE', payload: calculatePercent() });
}, [learnedCards.length, state.cards]);
State
const initialState = {
cards: [], <-- each card will be an object
percentComplete: 0,
lessonComplete: false,
};
Solution: Working solution using the first example:
const newCardsArray = [...state.cards];
const matchingCard = newCardsArray[action.payload];
matchingCard.correct += 1;
matchingCard.lastPass = true;
return { ...state, cards: newCardsArray };
Why: Spreading the array state.cards creates a new shallow copy of that array. Then I can make modifications on that cloned array and return it as the new value assigned to state.cards. The spread array has a new reference and that is detected by useEffect.
My best guess is that in the second working example .map returns a new array with a new reference. In the first example you are just mutating the contents of the array but not the reference to that array.
I am not exactly sure how useEffect compares, but if I remember correctly for an object it is just all about the reference to that object. Which sometimes makes it difficult to use useEffect on objects. It might be the same with arrays too.
Why dont you try out:
const newCardsArray = [...state.cards]
// do your mutations here
should copy the array with a new ref like you did with the object.
I have a reducer which is giving me some slowness. I have identified the step which clones/copies a part of my state as the slow step.
export default function itemReducer(state = initialState, action) {
case ITEM_FETCH_IMPACT_UPDATE:
{
let index = action.payload.index
slow step-> let items = [...state.items];
items[index] = {...items[index], overallIsLoading: true};
return {...state, items}
};
}
items is a fairly large array of about 300 objects, with each object having ~10 properties. How should I go about speeding this up while maintaining best Redux/React practices?
That's a use case for using Immer for Writing Immutable Updates.
Writing immutable update logic by hand is frequently difficult and prone to errors. Immer allows you to write simpler immutable updates using "mutative" logic, and even freezes your state in development to catch mutations elsewhere in the app. We recommend using Immer for writing immutable update logic, preferably as part of Redux Toolkit.
With Immer drafts, or any immutable library, such code will become:
const index = action.payload.index;
state.items[index].overallIsLoading = true;
And it will patch the state and return immutable data.
See the recommendation on Redux Style Guide.
I think copying is done twice, first time when you declare items, then when you return a new state.
If splitting the state itself is not a viable option, then my suggestion is to reduce the number of copying to once:
export default function itemReducer(state = initialState, action) {
case ITEM_FETCH_IMPACT_UPDATE:
return {
...state,
items: state.items.map((item, ind) => {
if(ind===action.payload.index){
return {
...item,
overallIsLoading:true
}
} else {
return item;
}
})
}
}
If I try that code with 3000 items then it still runs in microseconds. I'm not sure you identified the problem correctly or just provided the wrong code.
const a = new Array(3000).fill('').map(() =>
(
'abcdefghijklmnopqrstuvwxyz1' +
'234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ'
)
.split('')
.reduce((result, key) => {
result[key] = Math.random();
return result;
}, {})
);
const now = performance.now();
const b = [...a]
const c = [...a]
const d = [...a]
const e = [...a]
const f = [...a]
console.log('that took:',performance.now()-now)
I am using useSelector() to get state and when I apply some filter to local state variable, my global state is mutating. Not sure why. Below is the code.
const filterVacations = (employees, status) => {
if(status === 'Approved') {
employees.forEach(item => {
item.vacations = item.vacations.filter(vac => vac.approved === true);
});
}
else if(status === 'Unapproved') {
employees.forEach(item => {
item.vacations = item.vacations.filter(vac => vac.approved === false);
});
}
return employees.filter(item => item.vacations.length > 0);
}
and calling this function as below:
const Vacations = () => {
const state = useSelector((state:State) => state);
const view = state.view;
const filter = state.filter;
const employees = state.employees;
employees = filterVacations(employees, filter);
return (
<>
//some component...
</>);
}
why is state is mutating here?
Its because you are not passing value when you say const filter = state.filter; but its passing reference.
For example, consider a Bookshelf having JS book on top of it in a Library. When you visit Libraria doesn't give you the copy of book but the location of the book. So if you tear page of the book and keep it same place. The librarian can aslo see torn page. But if you dont want this to happen. You need to get copy of the book and tear pages from it while the book on shelf will remain as it is.
So when you want to make the copy of the data and store it in a variable ES6 has introduced easy way to do it than earlier clumsy way. so this const filter = state.filter;
will become this const filter = [...state.filter]; // this makes copy of array
I found the issue. useSelector will return a shallow copy of the nested objects, hence the mutation. The solution is to deep copy manually.
Please correct me if I'm wrong, because I've heard different stories with using redux with react. I've heard that you should have all your logic in your reducers, I've also heard that the store should be your single source of truth.
That said, I'm taking the approach where my logic, that is filtering stuff from json file is in a reducer file. Then I call the actions to filter out different parts of the json and return it,
import Data from "./BookOfBusiness.json";
const initialState = {
TotalData: Data
};
const filterDataWTF = (state = initialState, action) => {
let newState = { ...state };
let itExport = newState.TotalData;
if (action.type === "Status55") {
let itExport22 = {...itExport};
console.log("came to Status55 and itExport value is " + itExport22); // comes undefined. WHY??
return itExport22.filter(xyz => {
if (xyz.Status55.includes("Submitted")) {
return true;
}
return false;
});
}
return itExport;
};
export default filterDataWTF;
the problem is my variable itExport22 is showing up as undefined. Any tips please. TYVM
Okay once we got something working that you can vote for an answer, lets write an answer for you.
In that case your filter is returning a boolean. You should do something like so:
return itExport22.filter(xyz => xyz.Status55.includes("Submitted"));
Returning an empty array you are sure that you are receiving the same type on your component.
Your filter is fine. Only this should work
Your reducer should always return the new state
your reducer return type is boolean or itExport22 but it's should be object contains TotalData
I've recently started to try to learn React hooks, but for the life of me I can't figure out some things, like multiple state management, I've found a few example but nothing seems to be in a specific pattern. I'm not even sure how you're supposed to define your states if there's more than 2 and you want to change them individually, should it be like this:
const [slides, setSlides] = useState([])
const [currentSlide, setCurrentSlide] = useState(0)
const [tagName, setTagName] = useState([])
Or like this:
const [states, setStates] = useState({
slides: [],
currentSlide: 0,
tagName: []
})
And if both or the second one is viable (honestly I would prefer the second one since its less repetitive in calling useState and closer to the standard state meta) how could one go about changing states in such a example? I couldn't really find anything on this, so what I tried was this:
useEffect(() => {
Object.entries(Slides).forEach(slide => {
setStates(prevState => ({ slides: [...prevState.slides, slide[1]] }))
})
}, [])
And I messed around with it a bunch trying to get some decent results but I couldn't get it to work properly.
Any ideas of what I'm doing wrong? And on which one of these to methods of best practice?
Thanks!
In terms of updating state, first template is much easy and simple, because each stateUpdate function will be responsible for updating single value, that can be obj/array/int/bool/string. Another thing is, to access each value of the state (it will be an object), you need to write states.slides, states.tagName etc.
With first case, state update will be simply:
// only value in case of int/string/bool
updateState({ [key]: value }) or updateState(value);
But in second case, state will be an object with multiple keys, so to update any single value you need to copy the object, then pass the updated key-value. Like this:
updateState({
...obj,
[key]: newValue
})
To update the slides array:
updateState(prevState => ({
...prevState,
slides: newArray
}))
Handling complex state update:
Use useReducer to handle the complex state update, write a separate reducer function with action types and for each action type so the calculation and return the new state.
For Example:
const initialState = { slides: [], tagName: [], currentSlide: 0 };
function reducer(state, action) {
switch (action.type) {
case 'SLIDES':
return { ... };
case 'TAGNAME':
return { ... };
case 'CURRENT_SLIDE':
return { ... }
default:
throw new Error();
}
}
function Counter({initialState}) {
const [state, dispatch] = useReducer(reducer, initialState);
....
}
It is true that the useState hook can become quite verbose when dealing with complex state object.
In this case, you can also consider using the useReducer hook.
I would recommend the reading of this article about useState vs useReducer.