简体   繁体   中英

How to setState of nested JSON array object of mapped input

I have a JSON file with several categories, each category has a name with a set of input fields with their own name and value.

How can I use setState to update the value fields of each onChange ? The categories and fields are rendered using map() .

I am able to make it work without the nested fields but not with. Appreciate any assistance.

JSON File

[{
    "catName": "Category 1",
    "fields": [
      {
        "name": "field 1",
        "amount": "0"
      },
      {
        "name": "field 2",
        "amount": "0"
      }
    ]
  },
  {
    "catName": "Category 2",
    "fields": [
      {
        "name": "field 1",
        "amount": "0"
      },
      {
        "name": "field 2",
        "amount": "0"
      }
}]

Main.js

import React, { Component } from "react";
import Category from "./Category";
import sampleData from "./sampleData";

class Main extends Component {
  constructor(props) {
    super(props);
    this.state = {
      list: sampleData
    };
  }

  handleChange = e => {
    this.setState({ ???  });
  };

  render() {
    return (
      <div>
        {this.state.list.map(item => (
          <Category
            id={item.catName}
            name={item.catName}
            key={item.catName}
            list={item}
            handleChange={this.handleChange}
          />
        ))}
      </div>
    );
  }
}

export default Main;

Category.js

import React from "react";
import Item from "./Item";

const Category = ({ name, list, handleChange }) => {
  return (
    <div className="section">
      <h3>{name}</h3>
      {list.fields.map(item => (
        <Item
          id={item.name}
          name={item.name}
          key={item.name}
          list={item}
          handleChange={handleChange}
        />
      ))}
    </div>
  );
};

export default Category;

Item.js

import React from "react";

const Item = ({ list, handleChange }) => {
  return (
    <div className="item">
      <label className="label">{list.name}</label>
      <input
        name={list.name}
        id={list.name}
        className="input"
        type="text"
        onChange={handleChange}
        value={list.amount}
      />
    </div>
  );
};

export default Item;

Your JSON is invalid. You also forgot to check if list already contains any data.

Try this:

In your handleChange method make sure to use correct JSON markup. You forgot the closing ]} :

 this.setState({ list: [{
      "catName": "Category 1",
      "fields": [
        {
          "name": "field 1",
          "amount": "0"
        },
        {
          "name": "field 2",
          "amount": "0"
        }
      ]
    },
    {
      "catName": "Category 2",
      "fields": [
        {
          "name": "field 1",
          "amount": "0"
        },
        {
          "name": "field 2",
          "amount": "0"
        }
    ]}
  ]})

Inside the render method of your Main class check if the list is an array and if its length is bigger than 0. This will prevent any render errors, in case a non array type of value is set.

   {Array.isArray(this.state.list) && this.state.list.length < 0 && this.state.list.map(item => (
      <Category
        id={item.catName}
        name={item.catName}
        key={item.catName}
        list={item}
        handleChange={this.handleChange}
      />
    ))}

Also make sure to set an empty array inside the constructor of your Main Class:

constructor(props) {
    super(props);
    this.state = {
      list: []
    };
 }

let's start from bottom up

  1. you need to supply to Item.js the id of its parent category by changing is id to id={${name},${item.name}} . also will be nice to add onClick event to clean the previous data

  2. the category component need to supply is id to the item component

  3. then in the main component after you get the right access to the json you can use the createNewData methood to create new object

this is the result:

Main.js

import React, { Component } from "react";
import Category from "./Category";
import sampleData from "./sampleData";

class Main extends Component {
  constructor(props) {
    super(props);
    this.state = {
      list: sampleData
    };
  }

  createNewData = (mainAccess, property, value) => {
    let newData = sampleData;
    newData.forEach(category => {
      if (category["catName"] === mainAccess) {
        debugger;
        category["fields"].forEach(item => {
          if (item["name"] === property) {
            console.log(item["amount"]);
            item["amount"] = value;
          }
        });
      }
    });
    return newData
  };

  handleChange = e => {
    const propertyAccess = e.target.id.split(",");
    const newData = this.createNewData(propertyAccess[0],propertyAccess[1],e.target.value)
    this.setState({list:newData})
  };

  render() {
    return (
      <div>
        {this.state.list.map(item => (
          <Category
            id={item.catName}
            name={item.catName}
            key={item.catName}
            list={item}
            handleChange={this.handleChange}
          />
        ))}
      </div>
    );
  }
}

export default Main;

Item.js

import React from "react";


const Item = ({ list, handleChange ,id}) => {

    return (
    <div className="item">
      <label className="label">{list.name}</label>
      <input
        name={list.name}
        id={id}
        className="input"
        type="text"
        onChange={handleChange}
        onClick={e=>e.target.value=""}
        value={list.amount}

      />
    </div>
  );
};

export default Item;

Category.js

import React from "react";
import Item from "./Item";

const Category = ({ name, list, handleChange }) => {
  return (
    <div className="section">
      <h3>{name}</h3>
      {list.fields.map(item => (
        <Item
          id={`${name},${item.name}`}
          name={item.name}
          key={item.name}
          list={item}
          handleChange={handleChange}
        />
      ))}
    </div>
  );
};

export default Category;

Update your code as follows

import React, { Component } from "react";
import Category from "./Category";
import sampleData from "./sampleData";

class Main extends Component {
  constructor(props) {
    super(props);
    this.state = {
      list: sampleData
    };
  }

  handleChange = (e, fieldName, catName) => {
    //get list from state
    const { list } = this.state

    //this returns the related item's index, I assume that all cats have a unique name, otherwise you should use unique values such as IDs
    const targetCatIndex = list.findIndex(item => item.catName === catName) 

    //find related field index
    const targetFieldIndex = list[targetCatIndex].fields.findIndex(item => item.name === fieldName)

    //update the field and assign to state
    list[targetCatIndex].fields[targetFieldIndex].amount = e.target.value

    this.setState({ list: list  });
  };

  render() {
    return (
      <div>
        {this.state.list.map(item => (
          <Category
            id={item.catName}
            name={item.catName}
            key={item.catName}
            list={item}            
            handleChange={this.handleChange}
          />
        ))}
      </div>
    );
  }
}

export default Main;



import React from "react";
import Item from "./Item";

const Category = ({ name, list, handleChange }) => {
  return (
    <div className="section">
      <h3>{name}</h3>
      {list.fields.map(item => (
        <Item
          id={item.name}
          name={item.name}
          key={item.name}
          list={item}
          // pass field and cat referance with input event
          handleChange={(e, fieldName) => handleChange(e, fieldName, name) } 
        />
      ))}
    </div>
  );
};

export default Category;


import React from "react";

const Item = ({ list, handleChange }) => {
  return (
    <div className="item">
      <label className="label">{list.name}</label>
      <input
        name={list.name}
        id={list.name}
        className="input"
        type="text"
        //pass related field referance here
        onChange={(e) => handleChange(e, list.name)}
        value={list.amount}
      />
    </div>
  );
};

export default Item;

And here is the working demo

Pass the category and item index to your handleChange function. Use those index to update the correct item in the array. Avoid state mutation by not doing

// state mutation
this.state.list[categoryIndex].fields[fieldIndex].amount = e.target.value

handleChange function

handleChange = (e, categoryIndex, itemIndex) => {

  const { list } = this.state;

  const fields = [...list[categoryIndex].fields.slice(0, itemIndex),
  Object.assign({}, list[categoryIndex].fields[itemIndex], { amount: e.target.value }),
  ...list[categoryIndex].fields.slice(itemIndex + 1)
  ]


  this.setState({
    list: [...list.slice(0, categoryIndex),
    Object.assign({}, list[categoryIndex], { fields }),
    ...list.slice(categoryIndex + 1)
    ]
  })
}

Item component, add category and filed index as props.

import React from "react";

const Item = ({ list, handleChange, categoryIndex, itemIndex, value }) => {
  return (
    <div className="item">
      <label className="label">{list.name}</label>
      <input
        name={list.name}
        id={list.name}
        className="input"
        type="text"
        value={value}
        onChange={(e) => handleChange(e, categoryIndex, itemIndex)}
      />
    </div>
  );
};

export default Item;

Category component

import React from "react";
import Item from "./Item";

const Category = ({ name, list, handleChange, categoryIndex }) => {
  return (
    <div className="section">
      <h3>{name}</h3>
      {list.fields.map((item, index) => (
        <Item
          id={item.name}
          name={item.name}
          key={item.name}
          list={item}
          categoryIndex={categoryIndex}
          itemIndex={index}
          value={item.amount}
          handleChange={handleChange}
        />
      ))}
    </div>
  );
};

export default Category;

DEMO

 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.0/umd/react-dom.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.21.1/babel.min.js"></script> <div id="root"></div> <script type="text/babel"> const Item = ({ list, handleChange, categoryIndex, itemIndex, value }) => { return ( <div className="item"> <label className="label">{list.name}</label> <input name={list.name} id={list.name} className="input" type="text" value={value} onChange={(e) => handleChange(e, categoryIndex, itemIndex)} /> </div> ); }; const Category = ({ name, list, handleChange, categoryIndex }) => { return ( <div className="section"> <h3>{name}</h3> {list.fields.map((item, index) => ( <Item id={item.name} name={item.name} key={item.name} list={item} categoryIndex={categoryIndex} itemIndex={index} value={item.amount} handleChange={handleChange} /> ))} </div> ); }; class App extends React.Component { constructor() { super(); this.state = { name: 'React', show: false, list: [ { "catName": "Category 1", "fields": [ { "name": "field 1", "amount": "0" }, { "name": "field 2", "amount": "0" } ] }, { "catName": "Category 2", "fields": [ { "name": "field 1", "amount": "0" }, { "name": "field 2", "amount": "0" } ] } ] }; } handleChange = (e, categoryIndex, itemIndex) => { const { list } = this.state; const fields = [...list[categoryIndex].fields.slice(0, itemIndex), Object.assign({}, list[categoryIndex].fields[itemIndex], { amount: e.target.value }), ...list[categoryIndex].fields.slice(itemIndex + 1) ] this.setState({ list: [...list.slice(0, categoryIndex), Object.assign({}, list[categoryIndex], { fields }), ...list.slice(categoryIndex + 1) ] }) } show = () => { this.setState({ show: true }) } render() { return ( <div> {this.state.list.map((item, index) => ( <Category id={item.catName} name={item.catName} key={item.catName} categoryIndex={index} list={item} handleChange={this.handleChange} /> ))} <br /> <button onClick={this.show}>Show changes</button> {this.state.show && <pre> {JSON.stringify(this.state.list, null, 4)} </pre> } </div> ); } } ReactDOM.render( <App />, document.getElementById('root') ); </script> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM