Why is this Redux state being mutated? - javascript

I am having a problem with prevProps and this.props evaluating as different, despite the structures visually looking the same. I am assuming it is something to do with the arrays, however even after stripping out any changes to the existing state in my reducer, the problem still persists. Within 'Settings' there is an array, but since I am not mutating anything here, I don't see why the comparison would fail in my react component,
case types.UPDATE_USER_SETTINGS_SUCCESS: {
return {
...state,
user: {
...state.user,
settings: {
...state.user.settings,
},
},
};
}
This is the code that I am then using to compare the state in UNSAFE_componentWillReceiveProps.
if (this.props.user.settings !== nextProps.user.settings) {
console.log('is different');
}
The console log is firing each time and I cannot work out why. Dev tools shows the same objects.
(I realise the reducer code won't actually change anything, but I have removed the action payload for now to demonstrate that I am still getting the same problem)

If I understood you correctly you have "is different" in the console log after the UPDATE_USER_SETTINGS_SUCCESS action is fired.
This is to be expected since you are mutating the state. In your reducer you write:
settings: {
...state.user.settings,
}
That means that you do in fact get a new object. This line does not seem to do anything though, so if you remove it, then it should work as expected. Note: you do not need to remove the whole reducer, because even though you do return a new state with some of the fields referencing new objects, if you remove settings spread you will get exactly the same reference to the settings.
P.S. The whole reducer is not doing anything as far as I can see apart from returning a different object for some of the fields. The values are identical, but the references are not so you will get !== to be true.

They always will differ because you make shallow comparison of object references:
// Always true in javascript
{ a: 1 } !== { a: 1 }
That's because you return a new object in the reducer:
case types.UPDATE_USER_SETTINGS_SUCCESS: {
// Thats a new object reference
return { /* state */},
};
}

Related

Why should i create a copy of the old array before changing it using useState in react?

Why this works
const handleToggle = (id) => {
const newTodos = [...todos]
newTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
});
setTodos(newTodos);
}
And this doesnt
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
}))
}
Why do i have to create a copy of the old todos array if i want to change some item inside it?
You are doing a copy of the array in both cases, and in both cases you are mutating the state directly which should be avoided. In the second version you also forgot to actually return the todo, so you will get an array of undefined.
Instead you should shallow copy the todo you want to update.
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
return {...todo, completed: !todo.completed}
}
return todo
}))
}
Why mutating state is not recommanded? source
Debugging: If you use console.log and don’t mutate state, your past
logs won’t get clobbered by the more recent state changes. So you can
clearly see how state has changed between renders.
Optimizations: Common React optimization strategies rely on skipping
work if previous props or state are the same as the next ones. If you
never mutate state, it is very fast to check whether there were any
changes. If prevObj === obj, you can be sure that nothing could have
changed inside of it.
New Features: The new React features we’re
building rely on state being treated like a snapshot. If you’re
mutating past versions of state, that may prevent you from using the
new features.
Requirement Changes: Some application features, like implementing
Undo/Redo, showing a history of changes, or letting the user reset a
form to earlier values, are easier to do when nothing is mutated. This
is because you can keep past copies of state in memory, and reuse them
when appropriate. If you start with a mutative approach, features like
this can be difficult to add later on.
Simpler Implementation: Because
React does not rely on mutation, it does not need to do anything
special with your objects. It does not need to hijack their
properties, always wrap them into Proxies, or do other work at
initialization as many “reactive” solutions do. This is also why React
lets you put any object into state—no matter how large—without
additional performance or correctness pitfalls.
In practice, you can often “get away” with mutating state in React,
but we strongly advise you not to do that so that you can use new
React features developed with this approach in mind. Future
contributors and perhaps even your future self will thank you!
when you're changing an object using a hook correctly (- in this case useState) you are causing a re-render of the component.
if you are changing the object directly through direct access to the value itself and not the setter function - the component will not re-render and it will likely cause a bug.
and the .map(function(...)=>{..}) is a supposed to return an array by the items that you are returning from the function within. since you're not returning anything in the second example - each item of the array will be undefined - hence you'll have an array of the same length and all the items within will be undefined.
these kinds of bugs will not happen if you remember how to use array functions and react hooks correctly,
it's usually really small things that will make you waste hours on end,
I'd really recommend reading the documentation.

Mutation payload changes value by itself in vuex store mutation

I'm trying to build an Electron app with VueJS using the electron-vue boilerplate. I have a mutation which updates parts of the state based on the payload it receives.
However, somewhere between the action call and the mutation, the property payload.myItem.id changes without any intention.
The action is called by a Vue modal component:
handleModalSave() {
let payload = {
layerId: this.layer.id,
myItem: {
id: this.editLayerForm.id,
text: this.editLayerForm.text
}
}
console.log('save', payload.myItem.id)
this.$store.dispatch('addLayerItem', payload)
}
Here are said action and mutation:
// Action
addLayerItem: ({ commit }, payload) => {
commit('ADD_LAYER_ITEM', payload)
}
// Mutation
ADD_LAYER_ITEM: (state, payload) => {
console.log('mutation', payload.myItem.id)
let layer = state.map.layers.find(layer => layer.id === payload.layerId)
if (payload.myItem.id !== '') {
// Existing item
let itemIdx = layer.items.findIndex(item => item.id === payload.myItem.id)
Vue.set(layer.items, itemIdx, payload.myItem)
} else {
// New item
payload.myItem.id = state.cnt
layer.items.push(payload.myItem)
}
}
Here is a screenshot of the console logs:
As far as I can see, there is no command to change myItem.id between console.log('save', payload) and console.log('mutation', payload). I use strict mode, so there is no other function changing the value outside of the mutation.
Edit
I updated the console.logs() to display the property directly instead of the object reference.
As far as I can see, there is no command to change myItem.id between console.log('save', payload) and console.log('mutation', payload).
It doesn't need to change between the console logging.
In the pictures you've posted you'll see a small, blue i icon next to the console logging. If you hover over that you'll get an explanation that the values shown have just been evaluated.
When you log an object to the console it grabs a reference to that object. It doesn't take a copy. There are several reasons why taking a copy is not practical so it doesn't even try.
When you expand that object by clicking in the console it grabs the contents of its properties at the moment you click. This may well differ from the values they had when the object was logged.
If you want to know the value at the moment it was logged then you could use JSON.stringify to convert the object into a string. However that assumes it can be converted safely to JSON, which is not universally true.
Another approach is to change the logging to directly target the property you care about. console.log(payload.myItem.id). That will avoid the problem of the logging being live by logging just the string/number, which will be immutable.
The line that changes the id appears to be:
payload.myItem.id = state.cnt
As already discussed, it is irrelevant that this occurs after the logging. The console hasn't yet grabbed the value of the property. It only has a reference to the payload object.
The only mystery for me is that the two objects you've logged to the console aren't both updated to reflect the new id. In the code they appear to be the same object so I would expect them to be identical by the time you expand them. Further, one of the objects shows evidence of reactive getters and setters whereas the other does not. I could speculate about why that might be but most likely it is caused by code that hasn't been provided.
I use strict mode, so there is no other function changing the value outside of the mutation.
Strict mode only applies to changing properties within store state. The object being considered here is not being held in store state until after the mutation. So if something were to change the id before the mutation runs it wouldn't trigger a warning about strict mode.
Okay I found the root cause for my issue. Vuex was configured to use the createPersistedState plugin which stores data in local storage. Somehow, there was an issue with that and data got mixed up between actual store and local storage. Adding a simple window.localStorage.clear() in main.js solved the problem!

Does JSON.parse(JSON.stringify(state)) guarantee no mutation of redux state?

I've been tracking down an issue where a redux store correctly updates, but the component does not reflect the change. I've tried multiple ways of making sure I'm not mutating the state, however the issue persists. My reducers commonly take the form:
case 'ADD_OBJECT_TO_OBJECT_OF_OBJECTS': {
newState = copyState(state);
newState.objectOfObjects[action.id] = action.obj;
return newState;
}
For my copyState function, I usually use nested Object.assign() calls, but avoiding errors isn't so straightforward. For testing, to make sure I'm not mutating state, is it correct to use
const copyState = (state) => {
return JSON.parse(JSON.stringify(state));
};
as a guaranteed method of not mutating (redux) state, regardless of how expensive the process is?
If not, are there any other deep copy methods that I can rely on for ensuring I'm not mutating state?
EDIT:
Considering that other people might be having the same issue, I'll relay the solution to the problem I was having.
I was trying to dispatch an action, and then in the line after, I was trying to access data from the store that was updated in the previous line.
dispatchAction(data) // let's say this updates a part of the redux state called 'collection'
console.log(this.props.collection) // This will not be updated!
Refer to https://github.com/reactjs/redux/issues/1543
Yes this does a deep clone, regardless of how expensive it is. The difficulty (as hinted in comment under the question) is to make sure state stays untouched everywhere else.
Since you're asking for other approaches, and that would not fit in a comment, I suggest to take a look at ImmutableJS, which eliminates the issue of tracking state mutation bugs (but might come with its own limitations):
const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1.get('b') // 2
map2.get('b') // 50
Your copyState() function will NOT mutate your state. It is fine. And for others reading this question, it is also fine to directly go
const newState = JSON.parse(JSON.stringify(oldState));
to get a deep copy. Feels unclean, but it is okay to do.
The reason you're not seeing your subscriptions run may be that Redux compares the new state (the output of the reducer function) to the existing state before it copies it. If there are no differences, it doesn't fire any subscriptions. Hence the reason that we never mutate state directly but return a copy -- if we did mutate state, changes would never be detected.
Another reason might be that we don't know what dispatchAction() in your code does exactly. If it is async (maybe because of middleware?) the changes wouldn't be applied until after your console.log() on the next line.

Updating Object Property in Reducer without Mutation

I feel like my reducer should be working, but it keeps insisting that I'm mutating the state.
Uncaught Error: A state mutation was detected inside a dispatch, in the path: output.outputList.0.composition. Take a look at the reducer(s) handling the action {"type":"SET_OUTPUT_COMPOSITION",
I posted something similar a couple hours ago with no answers, but I figured my redux state was too complicated. This is my simplified version and I'm still getting mutate errors.. what am I doing wrong? should I not be using a class in my redux state? should i be using some sort of immutable library? please help me.
My Initial Redux State
output: {
outputList: [], //composed of Output class objects
position: 0
}
Output Class
class Output {
constructor(output) {
this.id = output.id;
this.composition = output.getComposition();
this.outputObj = output;
this.name = output.name;
this.url = output.getUrl();
}
}
export default Output;
Reducer for updating property
case types.SET_OUTPUT_COMPOSITION: {
let outputListCopy = Object.assign([], [...state.outputList]);
outputListCopy[state.position].composition = action.composition;
return Object.assign({}, state, {outputList: outputListCopy});
Action
export function setOutputComposition(comp) {
return { type: types.SET_OUTPUT_COMPOSITION, composition: comp}
}
The spread operator does not deep copy the objects in your original list:
let outputListCopy = Object.assign([], [...state.outputList]);
It is a shallow copy, therefore
outputListCopy[state.position].composition = action.composition;
You are actually mutating previous state objects, as you said in your comment there are several ways to work around this, using slice/splice to create new instance of the array, etc.
You can also take a look at using ImmutableJS, in general I would say storing classes in the redux store makes the thing a bit hard to understand, I tend to favor simple structures that can be easily inspected with redux-tools.
The error is coming from dispatch. So it not even getting as far as the reducer. I expect it does not like you using class to define output. Instead just do const output ={ ... }.

ReplaceReducer causing unexpected key error

I have a React app that dynamically loads a module, including the module's reducer function, and then calls Redux's replaceReducer to, well, replace the reducer. Unfortunately I'm getting an error of
Unexpected key "bookEntry" found in initialState argument passed to createStore. Expected to find one of the known reducer keys instead: "bookList", "root". Unexpected keys will be ignored.
where bookEntry was a key on the older reducer that's getting replaced. And starting with the bookEntry module and switching to bookList causes this inverse error
Unexpected key "bookList" found in initialState argument passed to createStore. Expected to find one of the known reducer keys instead: "bookEntry", "root". Unexpected keys will be ignored.
The code is below - un-commenting the commented code does in fact fix this, but I'm guessing it shouldn't be needed.
Am I doing something else wrong with Redux that's making this code necessary?
function getNewReducer(reducerObj){
if (!reducerObj) return Redux.combineReducers({ root: rootReducer });
//store.replaceReducer(function(){
// return {
// root: rootReducer()
// }
//});
store.replaceReducer(Redux.combineReducers({
[reducerObj.name]: reducerObj.reducer,
root: rootReducer
}));
}
In general we don’t suggest you to “clean up” the data when changing routes or loading new modules. This makes the application a little less predictable. If we’re talking about hundreds of thousands of records, then sure. Is this the volume of the data you plan to load?
If there’s just a couple of thousands of items on every page, there is no benefit to unloading them, and there are downsides associated with the complexity you add to the application. So make sure you’re solving a real problem, and not optimizing prematurely.
Now, to the warning message. The check is defined inside combineReducers(). It means that unexpected state keys will be discarded. After you removed the bookEntry reducer that managed state.bookEntry, that part of the state was no longer recognized by the new root reducer, and combineReducers() logged a warning that it’s going to be discarded. Note that this is a warning, and not an error. Your code runs just fine. We use console.error() to make warning prominent, but it didn’t actually throw, so you can safely ignore it.
We don’t really want to make the warning configurable because you’re essentially implicitly deleting part of the application state. Usually people do this by mistake, and not intentionally. So we want to warn about that. If you want to get around the warning, your best bet is to write the root reducer (currently generated by combineReducers()) by hand. It would look like this:
// I renamed what you called "root" reducer
// to "main" reducer because the root reducer
// is the combined one.
let mainReducer = (state, action) => ...
// This is like your own combineReducers() with custom behavior
function getRootReducer(dynamicReducer) {
// Creates a reducer from the main and a dynamic reducer
return function (state, action) {
// Calculate main state
let nextState = {
main: mainReducer(state.main, action)
};
// If specified, calculate dynamic reducer state
if (dynamicReducer) {
nextState[dynamicReducer.name] = dynamicReducer.reducer(
nextState[dynamicReducer.name],
action
);
}
return nextState;
};
}
// Create the store without a dynamic reducer
export function createStoreWithoutDynamicReducer() {
return Redux.createStore(getRootReducer());
}
// Later call this to replace the dynamic reducer on a store instance
export function setDynamicReducer(store, dynamicReducer) {
store.replaceReducer(getRootReducer(dynamicReducer));
}
However the pattern we recommend is to keep the old reducers around.

Categories

Resources