0

I am trying to update array with objects using useCallback but instead of updating object it is adding or appending one too.

const items = [
       {
           id: 101,
           name: 'Mae Jemison',
       },
       {
           id: 201,
           name: 'Ellen Ochoa',
       },
   ];
const [headings, setHeadings] = useState(items);

const handleHeadingTextChange = useCallback(
       (value, id) => {
           let items2 = headings;
           items2 = items2.map((item, key) => {
               if (items2[key].id == id) {
                   items2[key].name = value
               }
               return items2
           });

           setHeadings((prevState) => [...prevState, items2]) // adding, not updating
           //setHeadings((prevState) => [...prevState, ...items2]) // try 2 : still adding
       },
       [setHeadings],
   );

<input type="text" id="101" value="" onChange={handleHeadingTextChange} />

So, on change expected output is

items = [
        {
            id: 101,
            name: 'Johaan',
        },
        {
            id: 201,
            name: 'Ellen Ochoa',
        },
    ];

But Instead I am getting

items = [
        {
            id: 101,
            name: 'Johaan',
        },
        {
            id: 201,
            name: 'Ellen Ochoa',
        },
       [{
            id: 101,
            name: 'Johaan',
        },
        {
            id: 201,
            name: 'Ellen Ochoa',
        }]
    ];

I am not sure how to set value in setHeading function so that it only update the value and not appending one too. Is there way to make it update only?

armin
  • 176
  • 5
  • 16
  • 1
    i mean, you told it to insert itself into itself. so it did that. If, within an Array.map you return the initial array, the initial array will now be a value of the new array that is returned from Array.map – Kevin B Feb 09 '23 at 18:51

1 Answers1

1

problems

  1. .map iterates over the entire array. that doesn't make sense if you are trying to just update a single item.
  2. onChange takes an event handler, but your handler accepts (value, id)
  3. the value property of your <input> should be set from the object's name property

solution

function MyComponent() {
  const [headings, setHeadings] = useState(items);

  const updateName = key => event => {
    setHeadings(prevState => {
      return [
        // elements before the key to update
        ...prevState.slice(0, key),

        // the element to update
        { ...prevState[key], name: event.currentTarget.value },

        // elements after the key
        ...prevState.slice(key + 1),
      ]
    })
  }

  return headings.map((h, key) =>
    <input key={key} id={h.id} value={h.name} onChange={updateName(key)} />
  )
}

improvement

Imagine having to write that function each time you have a component with array or object state. Extract it to a generic function and use it where necessary -

// generic functions
function arrUpdate(arr, key, func) {
  return [...arr.slice(0, key), func(arr[key]), ...arr.slice(key + 1)]
}

function objUpdate(obj, key, func) {
  return {...obj, [key]: func(obj[key])}
}

// simplified component
function MyComponent() {
  const [headings, setHeadings] = useState(items);

  const updateName = key => event => {
    setHeadings(prevState =>
      // update array at key
      updateArr(prevState, key, elem =>
        // update elem's name property
        updateObj(elem, "name", prevName =>
          // new element name
          event.currentTarget.value
        )
      )
    )
  }

  return headings.map((h, key) =>
    <input key={key} id={h.id} value={h.name} onChange={updateName(key)} />
  )
}

multiple arrow functions?

Curried functions make writing your event handlers easier, but maybe you've never used them before. Here's what the uncurried version would look like -

function MyComponent() {
  const [headings, setHeadings] = useState(items);

  const updateName = (key, event) => {
    setHeadings(prevState =>
      // update array at key
      updateArr(prevState, key, elem =>
        // update elem name property
        updateObj(elem, "name", prevName =>
          // new element name
          event.currentTarget.value
        )
      )
    )
  }

  return headings.map((h, key) =>
    <input key={key} id={h.id} value={h.name} onChange={e => updateName(key, e)} />
  )
}
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Thanks!! Your answer does make sense. I found issue in my code. I am using shopify's RenderItem and my input is in that function thats why it is adding and not updaing the state even with above code snippet. If I don't use RenderItem component then state update is fine. – armin Feb 10 '23 at 13:56