You are violating a rule on reducers. The rule that says must not mutate its input state argument. What you are doing here:
const productsReducer = (state = initialState, action) => {
let updatedNum = state.num;
let updatedQty = state.qty;
let index;
and even further down the lines of code for that productsReducer
. Now the rule is a bit misleading and somewhat wrong and I may get into what I mean, but with that said, keep in mind that we must not mutate that input argument of state
.
So for example all of this is bad:
export default (state, action) => {
state[0] = ‘Dan’
state.pop()
state.push()
state.name = ‘Dan’
state.age = 30;
};
If your reducer function has state
and an =
somewhere inside of it. You are violating that reducer rule and as a result having problems with your application.
So I think in order to solve your bug here, we need to understand this rule:
Why isn't my component re-rendering, or my mapStateToProps running? Accidentally mutating or modifying your state directly is
by far the most common reason why components do not re-render after an
action has been dispatched
The truth is you can mutate your state
argument all day and not see any error messages. You can add in properties to objects, you can change properties on objects, all on that state
argument and Redux will never give you any error message at all.
The reason why the Redux docs say not to mutate the state argument is because its easier to tell a beginner don't mutate that state ever than to tell them when they can and can't mutate that state.
To be clear, we should still follow Redux official docs of not mutating state
argument ever. We absolutely can, and you certainly did, but here is why you should follow what Redux docs is saying.
I want to help you understand the behind-the-scenes stuff that is going on with Redux so you can understand the bug in your application and fix it.
Let's take a look at the source code for the Redux library itself, specifically a snippet that begins on line 162 or so. You can check it out at: https://raw.githubusercontent.com/reduxjs/redux/master/src/combineReducers.js
Its the line that starts with let hasChanged = false
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
Study this block of code to really understand what this is saying:
accidental mutations prevent re-render after an action has been
dispatched
The code snippet above is the part of Redux that takes an action and anytime it gets dispatched and sends the action around to all the different reducers inside of your application.
So anytime we dispatch an action, the Redux code above is going to be executed.
I will walk you through this code step by step and get a better idea of what happens when you dispatch an action.
The first thing that happens is we set up a variable of hasChanged
and set it equal to false
.
Then we enter a for
loop which will iterate through all the different reducers inside of your application.
Inside the body of the for
loop there is a variable called previousStateForKey
That variable will be assigned the last state value that your reducer returned.
Every single time that a reducer gets called the first argument will be the state
that it returned the last time it ran.
So the previousStateForKey
is a reference to the previous state
value that a particular reducer that its iterating over returned. The next line is where the reducer gets invoked.
The first argument is the state
your reducer returned the last time it can, previousStateForKey
and then the second argument is the action
object.
Your reducer is going to run and then eventually turn some new state
value and that will be assigned to nextStateForKey
. So we have hasChanged
, previousStateForKey
and nextStateForKey
, which is our new state
value.
Immediately after your reducer runs and assigns that new state
value to nextStateForKey
, Redux will check to see if your reducer just returned a value of undefined which is what will happen when our reducers are first initialized and Redux checks this with the if
statement here:
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
That's one of the big rules with reducers, a reducer can never return a value of undefined. So with this if
statement, Redux asks, did they return undefined? If so throw this error message.
Assuming you get past that check, things start to get even more interesting at this line of code:
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
hasChanged
is going to take the value of a direct comparison between nextStateForKey
and your previousStateForKey
.
So that rule of not mutating your state
argument comes down to that line of code above.
What that comparison does is to check to see if nextStateForKey
and previousStateForKey
are the exact same array or object in memory.
So if you just returned some array and its the exact same array in-memory from the last time the reducer ran then hasChanged
will be a value of false, otherwise if your reducers returned a brand new array created in your reducer and the array is totally different from the array the last time your reducer ran, then hasChanged
will be equal to true.
So this hasChanged
variable is saying has any of the state returned by this reducer changed?
So the for
loop will iterate over all of your reducers and hasChanged
is going to be either true
or false
considering all of those different reducers that you passed to it.
Notice how there is only one hasChanged
variable? There is not one for every reducer. If any reducer has changed a value or returned a different value, array or object or a different value for an integer, number or string, hasChanged
will be equal to true
.
After the for
loop, things still get more interesting. We look at the value of hasChanged
and if it has been changed to be true, the result of the entire function is to return the new state
object. The entire new state
object assembled by all your different reducers, otherwise, if hasChanged
is equal to false
, we instead return state
and state
is a reference to all the state
that your reducers returned the last time they ran.
In summary, that snippet of code from Redux checks to see if after running your reducers, did any of your reducers return a brand new array or object or number or string, if they did, then Redux will return a brand new result from all your reducers, otherwise, if your reducers returned no new value, it will return the old state form your reducers.
What's the relevance here?
The reason its relevant to your issue is if Redux returns the old state value then Redux will not notify the rest of your application that data has changed.
If you do have a new state
, if you have mutated state
from one of your reducers and you return nextState
, Redux will look at that object and say oh we have some new state form all these different reducers and it will notify the rest of your application, including your React application that you have a new state available and that will cause your React application to re-render.
In summary, the reason you must not mutate your state argument in a reducer is not because you can't mutate it, the reason is that if you accidentally return the same value that is pumped into your reducer, if you say return state;
in your reducer and its still the same object or array, modified or not, Redux will say no difference here, its the same object or array in-memory, nothing has changed and so we have done no update to the data inside the application and the React app does not need to render itself and thats why you will not see any updated counter appear on the screen. Thats whats going on here.
Thats why the Redux docs tell you not to mutate state
argument in a reducer. If you return a state
argument making changes to it or not, if you return the same value in the form of return state;
then no change will be made to your application.
That is what the Redux docs is trying to convey with that rule there, do not mutate state
argument.
Its a lot easier to tell you not to mutate the state
argument such as it does in the Redux docs than to go through the huge answer I just gave you.