开发者问题收集

Redux 无法正确更新状态——尽管在前一行中添加了一个对象,但状态仍为空数组

2020-02-03
1734

我正尝试使用通常的做法来更新我的 React Redux 状态。您将当前状态不可变地复制到返回函数中,然后覆盖将具有新值的属性。

我预期的结果是

state.copy

更新为

[
    { webCopy: "", id: 0 },
    { webCopy: "", id: 1 }
]

不幸的是,尽管将复制值作为包含 JS 对象的新数组返回,但它仍然是一个空数组。

我尝试在 StackOverflow 搜索结果中查找 “react redux 状态未正确更新” ,但似乎没有一个搜索结果符合我的情况。

在我的应用中,我甚至检查过:只有

case actionTypes.ADD_COMPONENT

(reducer 中的第一个)在应用的这一部分激活。它实际上是一个按钮点击,它会自行触发

ADD_COMPONENT

。没有任何其他减速器的活动可以用空的 [] 数组覆盖我的状态。

为什么我在 case actionTypes.ADD_COMPONENT 末尾最终得到一个空数组?

我的 console.log() 语句甚至显示JavaScript对象是 newCopy 的值,就在我返回新状态之前。

所以这里是reducer.js。我已经上传了完整的减速器,而不仅仅是发生错误的 case actionTypes.ADD_COMPONENT

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;

有什么建议可以尝试吗?我尝试将此案例添加到我的 Reducer 中并在 .ADD_COMPONENT 之后的行中激活它,但它仍然会产生一个空数组:

case actionTypes.PREP_COPY:

            let prepCopy = [...state.copy];
            prepCopy.push({ webCopy: "", id: action.payload });
            return {
                ...state,
                copy: prepCopy
            };

我为有问题的变量 newCopy 赋予了一个唯一名称,以便不使用全局范围。以防万一。

我还能展示哪些其他代码?只有 Reducer 才能影响 Redux 状态,并且除了 .ADD_COMPONENT 和(现在).PREP_COPY 之外没有其他代码在运行

编辑: 根据建议,我尝试在返回状态时将扩展运算符与变量一起使用。如果我仅在两个 Reducer 操作中使用扩展运算符,代码现在就可以工作。仅在其中一个操作中使用它仍然会产生一个空数组。像这样:

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]
            };

所以要么我同时使用这两个操作,要么什么都没有。字面意思:当我同时使用两者时,每个循环都会添加两个 JS 对象。当我只使用一个时,每个循环都会添加 0 个 JS 对象。Wut。

比如,我应该向 React 团队报告错误吗?

第二次编辑:这是一个完全正常工作的代码沙箱。单击按钮时检查 console.log 语句 https://codesandbox.io/s/lucid-heisenberg-iczww

2个回答

您是否尝试过将副本作为新数组返回?它以前为我解决了问题

您只需使用扩展运算符即可创建它:

  return {
    ...state,
    components: stateArray,
    uniqueIdCounter: idIteration,
    copy: [...newCopy]
  };

另一种类似的方法可以消除将值推送到临时变量的需要:

  return {
    ...state,
    components: stateArray,
    uniqueIdCounter: idIteration,
    copy: [...state.copy, { webCopy: "", id: action.payload[1] }]
  };
Ed Lucas
2020-02-03

问题出在您的 codesandbox 链接中的 Pallette.js 中。在第 29 行,您调用 this.props.copy.pop() ,这会从 copy 中删除最后一个元素。 props.copystate.copy 是同一个数组,因此每次调用 addComponent 时,您都会改变状态。如果您将此:

nextCopyId = this.props.copy.pop().id + 1;

更改为此:

nextCopyId = this.props.copy[this.props.copy.length - 1].id + 1;

或此:

nextCopyId = [...this.props.copy].pop().id + 1;

state.copy 不再是空数组。

一般来说,在使用 React 和 Redux 时,请确保尽可能避免使用可变方法。优先使用 concat ,而不是 pushunshift 。优先使用 mapfilterreduce (它们都返回数组的副本),而不是 forEachfor 循环(它们鼓励您改变数组)。有时,改变是不可避免的,但要小心谨慎,并意识到当您改变来自 props 的数组/对象时,您也会影响状态!

helloitsjoe
2020-02-03