问题
I'm trying to update my React Redux state using the usual practice. You immutably copy your current state into the return function and then overwrite the properties which are to have a new value.
My expected result is to have
state.copy
update to
[
{ webCopy: "", id: 0 },
{ webCopy: "", id: 1 }
]
Unfortunately, it's an empty array despite returning the copy value as a new array with a JS object inside.
I've tried looking at StackOverflow search results for "react redux state not updating correctly" but none of the search results seem to match my situation.
In my app I've even checked: Only
case actionTypes.ADD_COMPONENT
(the first one in the reducer) activates at this part of the app. It is literally a button click which triggers
ADD_COMPONENT
on its own. There is no activity from any other reducer that could be overwriting my state with an empty [] array.
Why am I ending up with an empty array at the end of case actionTypes.ADD_COMPONENT?
My console.log() statement even shows a JavaScript Object being the value for newCopy RIGHT BEFORE I RETURN a new state.
So here is reducer.js. I've uploaded the COMPLETE reducer instead of just case actionTypes.ADD_COMPONENT where the error is taking place:
import * as actionTypes from "./constants";
let component = "";
let idIteration = null;
let stateArray = [];
let tempCopy = [];
const initialState = {
components: [],
uniqueIdCounter: 0,
currentPage: 1,
copy: [],
siteURL: "/salespage/"
};
const reducer = (state = initialState, action) => {
switch (action.type) {
// you can probably ignore everything outside of this switch statement
// this is the important switch statement
case actionTypes.ADD_COMPONENT:
stateArray = [...state.components];
console.log("state:", state);
console.log("State Copy:", state.copy);
component = action.payload[0]; // will be "Header", "Headline", "Text Area", "Image", "Email Field", "Footer"
if (component === "Header") {
// append "Header" component to the beginning of the list
stateArray.unshift({
type: component,
id: state.uniqueIdCounter
});
} else {
// push component to the end of the list
stateArray.push({
type: component,
id: state.uniqueIdCounter
});
}
idIteration = state.uniqueIdCounter + 1;
// SEND HELP. How could state.copy possibly be anything other than a JS object after these lines?
let newCopy = [...state.copy];
newCopy.push({ webCopy: "", id: action.payload[1] });
console.log("TTTTTTTTT", newCopy);
return {
...state,
components: stateArray,
uniqueIdCounter: idIteration,
copy: newCopy // why doesn't this update the array to contain newCopy? it should.
};
// i don't know how any of this could possibly cause my state.copy to be equal to []
case actionTypes.SET_NEW:
console.log("Activating SET_NEW");
// uploads the newly reordered set of components to state
let uploadNewOrder = [...action.payload];
return {
...state,
components: uploadNewOrder
};
case actionTypes.DEL:
console.log("activating DEL");
// uploads the state less the deleted item
let uploadShortened = [...action.payload];
return {
...state,
components: uploadShortened
};
case actionTypes.PAGE_CHANGE:
console.log("activating PAGE_CHANGE");
stateArray = [...state.components];
return {
...state,
// action.payload is set in each page's ComponentDidMount()
currentPage: action.payload
};
case actionTypes.NEW_VAR:
console.log("activating NEW_VAR");
// case "NEW_VAR" fires from Customize's renderStateComponents()
stateArray = [...state.components];
tempCopy = Object.assign([], state.copy); // avoids the TypeError bug with [...state.copy]
// push an empty copy datapoint to state with a unique id to use in identifying which copy goes where in interface
let newInputFieldNumber = { webCopy: "", id: action.payload };
tempCopy.push(newInputFieldNumber);
return {
...state,
components: stateArray,
copy: tempCopy
};
case actionTypes.ADD_COPY:
console.log("activating ADD_COPY");
tempCopy = [...state.copy]; // immutably copy state.copy
let textToAdd = action.payload[0];
let indexToFind = action.payload[1];
for (let i = 0; i < tempCopy.length; i++) {
if (tempCopy[i].id === indexToFind) {
// Modify the JS object linked to the appropriate input field
tempCopy[i] = { webCopy: textToAdd, id: indexToFind };
}
}
return {
...state,
components: stateArray,
copy: tempCopy
};
case actionTypes.SET_URL:
console.log("activating SET_URL");
stateArray = [...state.components];
// TODO: handle cases like user entered www.etc.com and https://www.test.com
let domain = action.payload;
const notAllowed = [
"https://www.",
"www.",
".",
"www",
"com",
"net",
"org",
".com",
".net",
".org"
];
for (let i = 0; i < notAllowed.length; i++) {
if (domain.includes(notAllowed[i])) {
domain = domain.replace(notAllowed[i], "");
}
}
return {
...state,
components: stateArray,
siteURL: "/salespage/" + domain
};
default:
return state;
}
};
export default reducer;
Any suggestions what to try? I tried adding this case to my reducer and activating it in the line after .ADD_COMPONENT and it's still yielding an empty array:
case actionTypes.PREP_COPY:
let prepCopy = [...state.copy];
prepCopy.push({ webCopy: "", id: action.payload });
return {
...state,
copy: prepCopy
};
I gave the offending variable, newCopy, a unique name as to not use global scope. In case that matters somehow.
What other code could I show? Only a reducer can affect Redux state and there is no other code running besides .ADD_COMPONENT and (now) .PREP_COPY
EDIT: As per suggestions, I've tried using the spread operator with my variables while returning state. The code now works if I use the Spread operator in BOTH Reducer Actions only. Using it in just one of them still yields an empty array. Like so:
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.ADD_COMPONENT:
// edited out some code...
let newCopy = [...state.copy];
newCopy.push({ webCopy: "", id: action.payload[1] });
console.log("newCopy", newCopy);
return {
...state,
components: stateArray,
uniqueIdCounter: idIteration,
copy: [...newCopy]
};
case actionTypes.PREP_COPY:
console.log("State.copy:", state.copy);
let prepCopy = [...state.copy];
prepCopy.push({ webCopy: "", id: action.payload });
console.log("PREPCOPY:", prepCopy);
return {
...state,
copy: [...prepCopy]
};
So either I use both those Actions at once, or nada. Literally: When I use both, I get two JS Objects added per cycle. When I use only one, I get 0 JS objects added per cycle. Wut.
Like, should I report a bug to React's team?
2nd edit: here is a fully working code sandbox. Check the console.log statements when you click a button https://codesandbox.io/s/lucid-heisenberg-iczww
回答1:
The problem is in Pallette.js in your codesandbox link. On line 29, you call this.props.copy.pop(), which removes the last element from copy. props.copy is the same array as state.copy, so you're mutating state every time addComponent is called. If you change this:
nextCopyId = this.props.copy.pop().id + 1;
to this:
nextCopyId = this.props.copy[this.props.copy.length - 1].id + 1;
or this:
nextCopyId = [...this.props.copy].pop().id + 1;
state.copy is no longer an empty array.
In general when working with React and Redux, make sure to avoid mutative methods as much as possible. Prefer concat over push or unshift. Prefer map, filter, or reduce (which all return a copy of the array) over forEach or a for loop (which encourages you to mutate the array). Sometimes mutation is unavoidable, but be cautious and aware that when you mutate arrays/objects coming from props, you're affecting the state as well!
回答2:
Have you tried returning copy as a new array? It's solved problems for me before
You can just use the spread operator to create it:
return {
...state,
components: stateArray,
uniqueIdCounter: idIteration,
copy: [...newCopy]
};
Another, similar method would eliminate the need to push the value onto a temporary variable:
return {
...state,
components: stateArray,
uniqueIdCounter: idIteration,
copy: [...state.copy, { webCopy: "", id: action.payload[1] }]
};
来源:https://stackoverflow.com/questions/60031869/redux-not-updating-state-correctly-state-stays-as-empty-array-despite-adding