如何使用钩子在反应中更新嵌套 json 对象的状态

时间:2021-06-13 20:14:06

标签: javascript json reactjs react-hooks

我有一个这样的格式的全局状态:

{
  type: "container",
  items: [
    {
      type: "box"
    },
    {
      type: "container",
      items: [
        type: "container",
        items: [
          {
            type: "box",
            color: "green"
          },
          {
            type: "box",
            color: "red"
          },
          {
            type: "box"
          }
        ]
      ]
    }
  ]
}

上面的状态将产生下面的输出: output

如您所见,我们只有两个元素:容器和盒子。 容器有按钮,可以包含容器或盒子。 盒子只能改变它们的颜色。 要更好地理解,请观看this demo

我已经能够复制这种行为,但我希望能够提取应用程序的整个状态并能够恢复它。

恢复状态没有问题。如果我传递像上面这样的 json,它将起作用。 但是当谈到保存我当前的状态时,问题就出现了。 你看,当我尝试恢复我的状态时,我将嵌套对象作为道具传递,然后它成为该子容器的本地状态。但是当我改变那个子容器的状态时,它显然不会改变全局状态(上面的json)。

所以我尝试了两种方法来改变全局状态:

这是我的代码:

  1. 创建 onChange 方法,该方法将在某些本地状态更改时触发。然后将该方法作为道具传递,并通过将更改的状态作为参数传递来调用该方法。 (你可以在我的代码中看到这一点)
  2. 将嵌套对象的路径(索引列表)作为 prop 传递以恢复到表单 state.items[path[0]].items[path[1]].items[path[n]],然后通过 setState 钩子回调手动更改它。

在这两种情况下,我都得到

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

它只是冻结。

Container.js

import React, { useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import Box from './Box'

function Container({ current, path, onChange }) {
  const styles = ... some styles ...
  
  const [popoverVisible, setPopoverVisibility] = useState(false)

  const [state, setState] = useState(current || [])

  React.useEffect(() => onChange(state, path), [state])

  const handleButtonMouseOver = () => setPopoverVisibility(true)
  const handleButtonMouseLeave = () => setPopoverVisibility(false)
  const handlePopoverMouseOver = () => setPopoverVisibility(true)
  const handlePopoverMouseLeave = () => setPopoverVisibility(false)

  const props = {
    container: { style: styles.container },
    wrapper: { style: styles.wrapper },
    popover: {
      style: styles.popover,
      onMouseOver: handlePopoverMouseOver,
      onMouseLeave: handlePopoverMouseLeave
    },
    buttonAdd: {
      style: styles.button,
      onMouseOver: handleButtonMouseOver,
      onMouseLeave: handleButtonMouseLeave
    },
    buttonBox: {
      style: styles.button,
      onClick: () => setState([...state, {type: 'box'}])
    },
    buttonContainer: {
      style: styles.button,
      onClick: () => setState([...state, {type: 'container', items: []}])
      //onClick: () => setState({...state, items: [...state.items, {type: 'container', items: []}]})
    }
  }

  return (
    <>
      <div {...props.container}>
        {state.map((x, i) => x.type === 'container' ? <Container key={uuidv4()} current={x.items} onChange={ onChange } path={[...path, i]}></Container> : <Box key={uuidv4()}></Box>)}
        <div {...props.wrapper}>
          <div>
            <div {...props.popover}>
              {popoverVisible && <button {...props.buttonBox}>Box</button>}
              {popoverVisible && <button {...props.buttonContainer}>Container</button>}
            </div>
            <button {...props.buttonAdd}>Add</button>
          </div>
        </div>
      </div>
    </>
  )
}

export default Container

App.js

import { useState, useEffect } from 'react'
import Container from './Container.js'

function App() {
  const styles = {
    button: {
      margin: ".5em",
      width: "7em",
      height: "3em",
      border: "1px solid black",
      borderRadius: ".25em"
    }
  }

  const [state, setState] = useState({type: "container", items: []})
  const [newState, setNewState] = useState("")
  const [generatedJson, setGeneratedJson] = useState("")

  const handleContainerChange = (s, p) => setState({...state, items: s})
  const handleTextareaChange = e => setNewState(e.target.value)
  const handleBuildButtonClick = () => setState(JSON.parse(newState))
  const handleCreateJsonButtonClick = () => setGeneratedJson(JSON.stringify(state))

  //useEffect(() => {alert(state)}, [state])

  return (
    <>
      <div style={{ display: "flex" }}>
        <Container key={state} current={state.items} path={[]} onChange={ handleContainerChange }>
        </Container>
      </div>
      <textarea cols="30" rows="5" onChange={ handleTextareaChange }></textarea><br/>
      <button style={styles.button} onClick={ handleBuildButtonClick }>Build</button><br/>
      <button style={styles.button} onClick={ handleCreateJsonButtonClick }>Create JSON</button>
      <label key={generatedJson}>{generatedJson}</label>
    </>
  )
}

export default App

所以我想要的是在本地状态改变时更新全局状态。 示例:

globalState = {
  type: "container",
  items: [
    { type: "container", items: [] }
  ]
}

// that inner container from the global state
localState = { type: "container", items: [] }

// so when it changes to

localState = { 
  type: "container", 
  items: [
    {type: "box"}
  ]
}

// i want the global state to become

globalState = {
  type: "container",
  items: [
    { 
      type: "container", 
      items: [
        {type: "box"}
      ]
    }
  ]
}

我该怎么做?

1 个答案:

答案 0 :(得分:0)

问题

我认为主要问题是将 state 用作 ContainerApp 组件上的 React 键。每次 state 更新时,您都会指定一个新的 React 密钥,React 将通过卸载先前版本并安装新版本的 Container 来处理此问题。

<Container
  key={state} // <-- new key each state update
  current={state.items}
  path={[]}
  onChange={ handleContainerChange }
>
</Container>

每次 Container 挂载时,它都会实例化 state 并运行 useEffect,后者调用传递的 onChange 道具。

const [state, setState] = useState(current || []);

React.useEffect(() => onChange(state, path), [state]);

解决方案

删除 ContainerApp 上的 React 键。

<Container
  current={state.items}
  path={[]}
  onChange={ handleContainerChange }
/>