1

What is the best and clean way to alter Object Arrays?

I have a code that look´s like this.

const [get_post, set_post] = useState([
    {name: "First"},
    {name: "Second"},
    {name: "Third"},
  ])

I would like to add and edit keys, on a certain index. So I do it like this:

 <button onClick={()=>
    set_post([...get_post, get_post[0].content = "This is index zero" ])
 }> Manipulate! </button>

My result is this:

[
    {"name": "First", "content": "This is index zero"},
    {"name": "Second"},
    {"name": "Third"},
    "This is index zero" <-- This line is the problem
]

I have googled this a lot and this seems to be a common subject, however.

This post describe the same problem and solution with a keyed object, which doesn't help me.
React Hooks useState() with Object

This post support 3rd party libs and/or deep copying, which I suspect isn't the "right" way of doing it either.
Whats the best way to update an object in an array in ReactJS?

This thread also support a lot of deep copys and maps, which I suppose I don't need (It's an array, I'm should be able to adress my object by index)?
How do I update states `onChange` in an array of object in React Hooks

Another deep copy solution
https://codereview.stackexchange.com/questions/249405/react-hooks-update-array-of-object

The list goes on...

Basically I want the result I got without the extra line,

and if even possible:

  • Without deep copying the state to inject back in.
  • Without 3rd party libraries.
  • Without using a keyed object.
  • Without running a map/filter loop inside set_post.

Edit: The reason why map should be unnecessary in setPost.

In my particular scenario the Module that renders the getPost already is a map-loop. Trying to avoid nested loops.

(My logic simplified)

const [get_post, set_post] = useState([
    {name: "First"},
    {name: "Second"},
    {name: "Third"},
  ])

//Render module
//Fixed numbers of controllers for each Object in Array.

get_post.map((post, index)=> {
<>

  <button onClick={()=>
     set_post([...get_post, get_post[index].content = "Content 1" ])}
     }> 
     Controller 1
  </button>

  <button onClick={()=>
     set_post([...get_post, get_post[index].content = "Content 2" ])}
     }> 
     Controller 2
  </button>

  <button onClick={()=>
     set_post([...get_post, get_post[index].content = "Content 3" ])}
     }> 
     Controller 3
  </button>

//...

</>
})

RustyJames
  • 123
  • 7
  • _"Trying to avoid nested loops"_... you don't have nested loops. Calling `set_post()`, no matter what you do with the callback, executes completely separately to your `get_post.map()` callback – Phil Oct 18 '21 at 22:30

2 Answers2

1

If you just want to alter the first property, extract it from the array first.

You can use the functional updates method to access the current state value and return a new one with the changes you want.

set_post(([ first, ...others ]) => [{
  ...first,
  content: "This is index zero"
}, ...others])

To alter any particular index, you can map the current array to a new one, creating a new object for the target index when you reach it

let x = the_target_index

set_post(posts => posts.map((post, i) => i === x ? {
  ...post,
  content: `This is index ${x}`
} : post))

A slightly different version of this that matches what you seem to want to do in your answer would be

set_post(posts => {
  posts[x] = { ...posts[x], content: `This is index ${x}` }
  // or even something like
  // posts[x].content = `This is index ${x}`

  return [...posts] // clone the array
})
Phil
  • 157,677
  • 23
  • 242
  • 245
  • That is very clean, is there any way of applicating that on index x? – RustyJames Oct 18 '21 at 01:06
  • I see. it´s a good example of the map-solution, and fair enough, it might be the only way do this. Do you understand why I get the results i get in my index based solution? – RustyJames Oct 18 '21 at 01:20
  • @RustyJames because the result of an assignment operation is the value assigned which you're setting as the last element in your array. You've basically got `const val = "This is index zero"; get_post[0].content = val; get_post.push(val);` – Phil Oct 18 '21 at 01:21
  • Thanks for making that clear, I'm still curious if there are any other ways of doing this without the map. – RustyJames Oct 18 '21 at 01:38
  • @RustyJames what's wrong with using `map()`? One of the fundamental aspects of using state in React is that you treat it immutably and `map` does exactly that by providing a new array for `get_post`. I honestly can't think of an easier way to do so. – Phil Oct 18 '21 at 02:43
  • See my updated question. – RustyJames Oct 18 '21 at 15:08
  • you are a god @Phil – James Schulman May 03 '22 at 21:28
-1

I have found a solution that works!
I haven't seen this posted elsewhere so it might be interesting to look into.

To change or add a key/value to an object in an array by index just do this:

    <button onClick={() => {
      set_post( [...get_post , get_post[index].content = "my content" ] )
      set_post( [...get_post ] ) //<-- this line removes the last input
    }
      }>

As Phil wrote, the earlier code is interpreted as:

const val = "This is index zero"; 
get_post[0].content = val; 
get_post.push(val);

This seems to remove the latest get_post.push(val)

According to the React Docs, states can be batched and bufferd to for preformance

https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly

When React batches set_post it should behave like this.

If the one-line command

set_post( [...get_post , get_post[index].content = "my content" ] ) 

gives

const val = "This is index zero"; 
get_post[0].content = val; 
get_post.push(val);

The double inject would trigger the buffer and reinject the state at the end insted of array.push(). Something like this.

var buffer = get_post
buffer[0].content = val
//Other updated to get_post...
get_post = buffer //(not push)

There for, this should be a perfectly good solutions.

RustyJames
  • 123
  • 7
  • See [Using State Correctly](https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly) for why you shouldn't do this. The only reason this _works_ is because you aren't waiting for the first `set_post` to apply. – Phil Oct 18 '21 at 22:31
  • I get that React is "batching setState for preformance" and that "setState may be Asynchronous" according to the docs. But it still shouldn't alter the value of the first push even in a batch? It also works with several values as well, so it's not just the first set_post. – RustyJames Oct 18 '21 at 23:32