【问题标题】:Issue with state update approach for nested objects嵌套对象的状态更新方法问题
【发布时间】:2021-04-30 17:47:01
【问题描述】:

主要编辑

我有一个非常大的物体,深度为 3 层。我使用它作为模板在页面上生成组件并存储稍后使用的值,例如:

obj =
 { 
  "group": {
    "subgroup1": {
      "value": {
        "type": "c",
        "values": []
      },
      "fields_information": {
        "component_type": "table",
        "table_headers": [
          "label",
          "size"
        ],
      }
    },
    "subgroup2": {
      "value": {
        "type": "c",
        "values": []
      },
      "fields_information": {
        "component_type": "table",
        "table_headers": [
          "label",
          "size"
        ],
      }
    },
  },
 }

多亏了这一点,我可以动态生成视图,该视图作为模板存储在 DB 中。

我在两件事上苦苦挣扎。首先,根据用户输入的文本框、复选框等更新值。 我是这样做的:

    const updateObj = (group, subgroup, value) => {
        let tempObj = {...obj}
        tempObj[group][subgroup].value.value = value
        toggleObj(tempObj)
    }

我知道扩展运算符实际上并没有进行深度复制。但是,它允许我处理对象并稍后保存。这是一个问题吗?我必须 cloneDeep 还是很好? cloneDeep 会影响性能吗?

下面描述第二种情况

export const ObjectContext = React.createContext({
    obj: {},
    toggleObj: () => {},
});

export const Parent = (props) => {
    const [obj, toggleObj] = useState()
    const value = {obj, toggleObj}

    return (
        <FormCreator />
    )
}

const FormCreator = ({ catalog }) => {
    const {obj, toggleObj} = React.useContext(ObjectContext)

    return (<>
        {Object.keys(obj).map((sectionName, sectionIdx) => {
        const objFieldsInformation = sectionContent[keyName].fields_information
        const objValue = sectionContent[keyName].value
        ...
            if (objFieldsInformation.component_type === 'table') {
            return (
                <CustomTable 
                key={keyName + "id"}
                label={objFieldsInformation.label}
                headers={objFieldsInformation.table_headers}
                suggestedValues={[{label: "", size: ""}, {label: "", size: ""}, {label: "", size: ""}]}
                values={objValue.values}
                sectionName={sectionName}
                keyName={keyName}/>
            )
            }
        ...
        })}
    </>)
}

const CustomTable= (props) => {
    const { label = "", headers = [], suggestedValues = [], values, readOnly = false, sectionName, keyName } = props
    const {obj, toggleObj} = React.useContext(ObjectContext) 
    
    //this one WORKS
    useEffect(() => {
        if (obj[sectionName][keyName].value.type === "complex") {
            let temp = {...obj}
            temp[sectionName][keyName].value.values = [...suggestedValues]
            toggleObj(temp)
        }
    }, [])
    
    //this one DOES NOT
    useEffect(() => {

        if (obj[sectionName][keyName].value.type === "c") {
            let temp = {...obj, [sectionName]: {...obj[sectionName], [keyName]: {...obj[sectionName][keyName], value: {...obj[sectionName][keyName].value, values: [{label: "", size: ""}, {label: "", size: ""}, {label: "", size: ""}]}}}}
            toggleObj(temp)
        }
    }, [])
    
    return (
    //draw the array
    )
}

请参考 CustomTable 组件。 与上面的示例对象一样,我有 2 个要打印的 CustomTables。不幸的是,一个应该工作的 useEffect 不能正常工作。我观察到,该值字段仅为 Obj 中的最后一个“表”设置。当我做 obj 的浅拷贝时,它工作正常。但我担心将来可能会发生任何影响。

我对使用 createContext 也完全陌生,也许这就是问题所在。

感谢任何理解这种混乱的人:)

【问题讨论】:

  • 您的问题不清楚 - 首先,subGroup 是一个数组,因此 [subGroup].value 应该始终未定义。但是您的最后两个示例也几乎相同,除了第二个是可变的(并且您访问valuesitems),所以当您说一个有效但另一个无效时,我不知道您的意思。不必要的状态变化不可避免地会在以后导致静默错误 - 例如,如果您有依赖于 subGroup 的效果或记忆组件,则当该组的 value.values 发生突变时,它将不会运行。
  • @lawrence-witt 我只是把它作为一个参考例子。我对设置 undefined 等没有任何问题(提供的示例可能搞砸了)。我可以先提到“不可避免地会导致无声的错误”。可以?我正在制作用 const [obj, toggleObj] = useState() 定义的对象的“副本”,我正在更新 tempObj(它也在更新 obj),但我用 toggleObj 更新了状态,实际上它正在覆盖 obj。那么刚才obj的突变有关系吗?
  • 此外,我所做的大约 2 种更新(后一种有效)是我主要关心的问题。在触发 toggleObj 之前,我将正确的变异 obj 分配给 var 并在之前将其打印出来,并且看起来符合要求。但是,在祖父母中,当我尝试使用 useEffect(()=>{}) 监视所需对象的更改时,实际上什么也没发生。我想以这种方式呈现 3 个表格,并且行仅对最后一个可见。我认为描述它有点复杂。我会尝试用所有组件做一个正确的例子。
  • 只是一个旁注,但当状态结构具有更多复杂性/深度时,我个人更喜欢使用 useReducer 而不是 useState

标签: reactjs clone use-state createcontext


【解决方案1】:

主要问题似乎是您没有提供上下文。你所拥有的实际上是传递空白对象和 void 返回函数。因此,为什么调用它没有实际效果,但改变值会有。

export const ObjectContext = React.createContext({
    obj: {},
    toggleObj: () => {},
});

export const Parent = (props) => {
    const [obj, toggleObj] = useState({})
    const value = {obj, toggleObj}

    return (
        <ObjectContext.Provider value={value}>
           <FormCreator />
        </ObjectContext.Provider>
    )
}

理想情况下,您还可以让上面的这个组件环绕FormCreator 并将其呈现为props.children。这是为了防止每次调用toggleObj 时重新渲染整个子树。请参阅first part of this tutorial 了解典型模式。

关于改变状态的问题,在 React 中保持状态不可变是绝对重要的——至少,如果你使用 useState 或某种 reducer。状态突变引起的错误一直出现在 Stack Overflow 上,事实上我最近做了一个 codesandbox 来展示一些更常见的错误。

我也同意@SamuliHakoniemi 的观点,像这样的深度嵌套对象实际上更适合useReducer 钩子,甚至可能更进一步,建议这里需要像 Redux 这样的适当状态管理库。它将允许您细分化简器以针对实际更新的状态片段,如果或当它成为实际问题时,这将有助于降低深度克隆状态结构的性能成本。

【讨论】:

  • 感谢您的建议!我按照教程中的建议重写了对 setState(state => ({...state,....) 的更新,并且改变状态似乎已经解决。但是,我想问你一些关于“理想情况下你会...... ."。我将 FormCreatorProvider 作为单独的组件:&lt;FormCreatorContext.Provider value={{state, setState}}&gt; {props.children} &lt;/FormCreatorContext.Provider&gt; 并使用此提供程序包装了 FormCreator,例如:&lt;FormCreatorProvider selectedCatalog={selectedCatalog}&gt; &lt;FormCreator catalog={selectedCatalog} /&gt; &lt;/FormCreatorProvider&gt;
  • 现在的问题是,FormCreator 中的所有内容都被重新渲染(顺便说一句之前也是如此)。这可以避免吗?此外,我实际上将 redux 与 thunk 中间件用于所有异步调用(例如获取提到的模板)。上下文只是要测试的新事物。无论如何,使用 redux,我应该为这些操作创建单独的商店吗?此外,我正在渲染的这个对象是非常动态的。我想知道当每个对象的字段不同时如何准备动作器和减速器。
  • 对不起,我应该指定,任何使用上下文的组件都将被重新渲染。 props.children 模式的好处是,您可以使用上下文覆盖更多同级组件,而无需重新渲染它们,除非它们使用它。如果你只在这个地方使用它来避免螺旋钻,那么它就没有那么重要了。 Redux 本身就是另一个巨大的话题,可能超出了这个问题的范围 - starting here,阅读关于结构化 reducer 的整个部分将是一个好的开始!
猜你喜欢
  • 2020-05-22
  • 2020-04-27
  • 2019-06-13
  • 1970-01-01
  • 2020-11-13
  • 1970-01-01
  • 2018-11-08
  • 2018-03-07
  • 1970-01-01
相关资源
最近更新 更多