5

I have an react state with objects/array that is filled, but also then later I want to edit text inputs and edit the related fields on object also, i found no solutions until now.

So this is my state for example:

const [data, setData] = useState({
    working_hours: [
      {
        id: 1,
        description: 'Random Text',
        price: 100,

      },
      {
        id: 2,
        description: 'Text Random',
        price: 100,
      },
    ]
  });

Here is my Jsx:

{data.working_hours.map(item => 
<>
  <input type={text} value={item.description}
   onChange={(e) => handleChange(e)} />

  <input type={text} value={item.price}
   onChange={(e) => handleChange(e)} />
</>
)}

Here is what I tried:

function handleChange(e){
 const value = e.target.value;
 setData({...data, [...data.working_hours, e.target.value]})
}
modih65067
  • 109
  • 1
  • 6
  • Does this answer your question? [How to update nested state properties in React](https://stackoverflow.com/questions/43040721/how-to-update-nested-state-properties-in-react) – yuriy636 Feb 06 '22 at 09:08
  • No that much, this is based on component. I'm looking for function based anwsers. Beside that, this question has a different logic, on updating based on current id that is in the map function. – modih65067 Feb 06 '22 at 09:09

5 Answers5

6

You need to pass additional parameters to your handleChange as item ID which you want to update and property name because without these you will not be able to identify which property to update dynamically. This way you can use the same handleChange for multiple inputs.
See below code -

function handleChange(e, itemId, property) {
    const value = e.target.value;
    //copying data to temp variable so that we do not directly mutate original state
    const tempWorkingHours = [...data.working_hours];
    //findIndex to find location of item we need to update
    let index = tempWorkingHours.findIndex(item => item.id == itemId);
    // -1 check to see if we found that object in working hours
    if(index != -1){
       tempWorkingHours[index] = {
         ...tempWorkingHours[index], //keeping existing values in object
         [property]: value  //here property can be "price" or "description"
       }
    }
    
    setData({ ...data, working_hours: tempWorkingHours })
}

{
    data.working_hours.map(item =>
        <>
            <input type={text} value={item.description}
                onChange={(e) => handleChange(e, item.id, "description")} />

            <input type={text} value={item.price}
                onChange={(e) => handleChange(e, item.id, "price")} />
        </>
    )
}
Sagar Darekar
  • 982
  • 9
  • 14
  • 1
    Do you think we can avoid `tempWorkingHours` by doing something like so: `setData(prevData => ({...prevData, working_hours: prevData.working_hours.map(itm => itm.id === itemId ? {...itm, [property]: e.target.value} : itm)});` – jsN00b Feb 06 '22 at 09:49
  • This worked, but I wonder for example if we want to make it more dinamic, and based on the parameter we pass on the function on onChange control which object, for example if we have another object named `not_working_hours` beside the one `working_hours`, so what I mean is that to be bale to control and the object where you are putting data, I hope you got me. – modih65067 Feb 06 '22 at 09:49
  • @jsN00b and what do you think about my comment, can we then do that? – modih65067 Feb 06 '22 at 09:50
  • @modih65067 - Sure, you may make it dynamic to update `working_hours` or `not_working_hours` or another prop of `data`. Need to pass it as a parameter to `handleChange` and update it appropriately within the method. – jsN00b Feb 06 '22 at 09:51
  • sure let's see, I hope @sagardarekar a edit – modih65067 Feb 06 '22 at 09:54
  • Yes, we can do it more dynamic. just need to put simply `if` condition and update state. – Sagar Darekar Feb 06 '22 at 10:29
2

When you want to update the state of objects in the nested array, you must identify these objects and the property you want to update. Thus your handler should look like this.

function handleChange(index, property, value){
   // ...
}

The setData function will only trigger a rerender if you pass it a new object. Thus you should create a copy.

function handleChange(index, property, value) {
  const new_working_hours = [...data.working_hours]; // copy the array
  const new_working_hour = { ...data.working_hours[index] }; // copy the array item to change

  new_working_hour[property] = value; // set the new value
  new_working_hours[index] = new_working_hour; // assign the new item to the copied array

  setData({ working_hours: new_working_hours }); // return a new data object
}

Here is a working example. Click Run code snippet below.

const { useState } = React;

const App = () => {
  const [data, setData] = useState(initialState);

  function handleChange(index, property, value) {
    const new_working_hours = [...data.working_hours]; // copy the array
    const new_working_hour = { ...data.working_hours[index] }; // copy the array item to change

    new_working_hour[property] = value; // set the new value
    new_working_hours[index] = new_working_hour; // assign the new item to the copied array

    setData({ working_hours: new_working_hours }); // return a new data object
  }

  return data.working_hours.map((item, index) => (
    <div>
      <input
        type="text"
        value={item.description}
        onChange={(e) => handleChange(index, "description", e.target.value)}
      />

      <input
        type="text"
        value={item.price}
        onChange={(e) => handleChange(index, "price", e.target.value)}
      />
    </div>
  ));
};


const initialState = {
  working_hours: [
    {
      id: 1,
      description: "Random Text",
      price: 100
    },
    {
      id: 2,
      description: "Text Random",
      price: 100
    }
  ]
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
René Link
  • 48,224
  • 13
  • 108
  • 140
1
function handleChange(e){
 const value = e.target.value;
 const newData = {
   ...data,
   working_hours: [
     ...(data?.working_hours ? data.working_hours : {}),
     value
   ]
 };
 setData(newData);
}

Here is a simulation

let data = {
 working_hours: [
  {
    id: 1,
    field: "test",
  }
 ],
};

function handleChange(e){
 const value = e.target.value;
 const newData = {
   ...data,
   working_hours: [
     ...(data?.working_hours ? data.working_hours : {}),
     value
   ]
 };
 console.log('newData', newData);
}

const event = {
 target: {
  value: { id: 2, field: "test2" },
 }
};

handleChange(event);
Meddah Abdallah
  • 654
  • 1
  • 7
  • 25
1

The problem is that you are missing the part of indicating which object to update, you can do so by passing the index of the array or by the id that the object has. for example:

function handleChange(e){
 const value = e.target.value;
 const tempWorkingHours = data.working_hours.map((item) => { 
    if (item.id === value.id) {
      return {
         ...item,
          price: value
      } 
   }
   return item;
}
 setData({...data, working_hours: tempWorkingHours })
}

in that way, you are using a map to loop over the array and find the item you want to change(but by the temp variable that holds a copy of the data to avoid mutating the state). You can also pass the index to the handle change function:

{data.working_hours.map((item, index) => 
<>
  <input type={text} value={item.description}
   onChange={(e) => handleChange(e)} />

  <input type={text} value={item.price}
   onChange={(e,index) => handleChange(e, index)} />
</>
)}

And then in the handleChange use the index to access the relevant index to change the array. let me know if you want me to explain more with modification of your code, but I think the way I explain above with the id solution is nice.

Edit- the index version :

     function handleChange(e, index) {  
      const value = e.target.value; 
      const tempWorkingHours = [...data.working_hours];  
      tempWorkingHours[index].price = value;  
      setData({...data, working_hours: tempWorkingHours});
}

Fixed the first example, the return moved outside the if statement in case it's not the relevant id we want to modify.

Read more about forms from react's official docs: https://reactjs.org/docs/forms.html It doesn't matter if you are using function components to get the point, its the same. Only the way You are updating the state is with useState.

  class Reservation extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          isGoing: true,
          numberOfGuests: 2
        };
    
        this.handleInputChange = this.handleInputChange.bind(this);
      }
    
      handleInputChange(event) {
        const target = event.target;
        const value = target.type === 'checkbox' ? target.checked : target.value;
        const name = target.name;
    
        this.setState({
          [name]: value
        });
      }
    
      render() {
        return (
          <form>
            <label>
              Is going:
              <input
                name="isGoing"
                type="checkbox"
                checked={this.state.isGoing}
                onChange={this.handleInputChange} />
            </label>
            <br />
            <label>
              Number of guests:
              <input
                name="numberOfGuests"
                type="number"
                value={this.state.numberOfGuests}
                onChange={this.handleInputChange} />
            </label>
          </form>
        );
      }
    }
LironShirazi
  • 381
  • 1
  • 8
  • I tried to copy/paste this and something is missing in the syntax. – modih65067 Feb 06 '22 at 09:24
  • Can you update the index to the handleChange too? – modih65067 Feb 06 '22 at 09:25
  • You have two returns inside the if, can you check it? – modih65067 Feb 06 '22 at 09:29
  • `function handleChange(e, index){ const value = e.target.value; const tempWorkingHours = [...data.working_hours]; tempWorkingHours[index].price = value; setData({...data, working_hours: tempWorkingHours }); }` – LironShirazi Feb 06 '22 at 09:30
  • Uncaught TypeError: Cannot set properties of undefined (setting 'price') – recabos710 Feb 06 '22 at 09:33
  • You are right, my bad, modified the example. The return moved outside the if statement to indicate the relevant id to modify. – LironShirazi Feb 06 '22 at 09:35
  • Yes I checked it, it doesn't work, also I don't understand it, because beside the price we are managing and the description, and in other cases custom fields that might be added in future – modih65067 Feb 06 '22 at 09:36
  • You didn't mention it in your issue, But you need to pass additional property (like the name of the field) to handle change and then modify the relevant field you want. if you want to be generic for all inputs. – LironShirazi Feb 06 '22 at 09:39
  • 1
    Thank you, Liron, you did also contributed to much, and others, now its fixed. – modih65067 Feb 06 '22 at 09:45
  • Read about form inside react official its the exact thing you are looking for, I edited the answer with the relevant code example and a link. Good luck. – LironShirazi Feb 06 '22 at 09:45
1

change elements

{data.working_hours.map(item => 
<>
  <input name='description' type={text} value={item.description}
   onChange={(e) => handleChange(e,item.id)} />

  <input name='price' type={text} value={item.price}
   onChange={(e) => handleChange(e,item.id)} />
</>
)}

change handleChange


function handleChange(e,id){
 const value = e.target.value;
 const name= e.target.name;
 var dataitemIndex = data.working_hours.findIndex(x=>x.id == id);
data.working_hours[dataitemIndex] = {...data.working_hours[dataitemIndex],[event.target.name]: event.target.value};
 setData(data);
}
A.R.SEIF
  • 865
  • 1
  • 7
  • 25