4

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;

2

4 Answers 4

2

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>

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for your efforts and quick response, works perfectly for me. Much appreciated. Any chance you would be able to explain or provide some background regarding what exactly is happening in handleChange? thanks again
I'm using slice and object assign to create a new array instead of changing the array directly. Object.assign, .slice()
1

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

Comments

1

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

Comments

0

let's start from bottom up

  1. you need to supply to Item.js the id of its parent category by changing is id toid={${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;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.