0

I am using setState hook to set an object with an array similar to:

const [organisations, setOrganisations] = useState({
    items: [],
    hasMore: false,
});

items array looks similar to this:

[
0: {id: "2", name: "Test Internal 5", isInternal: true},
1: {id: "3", name: "Test Internal 6", isInternal: false}
]

i want to setState using spread syntax of item 1, setting isInternal property to true.

How can i do this?

Thanks

Paul
  • 219
  • 1
  • 3
  • 11

2 Answers2

2

Edit: As Brian Thompson pointed out, the code below does mutate state, it will work but it is not recommended. If you wish to avoid mutating state, read this stack overflow post on cloning objects (more specifically this answer).

Original:

You can hook into the current value by passing a function with a desired variable name like so:

setOrganisations((prevVal) => {
  prevVal.items[1].isInternal = true;
  return prevVal;
});

Here prevVal is the same as your organisations before changes happen, you can update the value then return it to the setState function which will update accordingly.

NOTE: The method below also mutates the state

setOrganisations((prevVal) => {
  let ret = prevVal;
  ret.items[1].isInternal = true;
  return ret;
});

As ret uses the same reference object.

mpmcintyre
  • 614
  • 5
  • 16
  • Like multiple other answers, this mutates state... – Brian Thompson Sep 16 '21 at 16:01
  • Anytime you return the same `prevVal` object, or setstate to itself, etc - its a red flag. The only way this can have an effect is if you're mutating the state object. – Brian Thompson Sep 16 '21 at 16:03
  • Edited, is that better? – mpmcintyre Sep 16 '21 at 16:07
  • 1
    Unfortunately no, it's actually the exact same just with an extra variable. `let ret = prevVal` does not make a copy. It only assigns the same object *reference* `prevVal` holds into the new `ret` variable. To make a new copy, you would need to use something like `Object.assign` or spread syntax (`{...prevVal}`). To make it more complicated, these methods only make a *shallow* copy. So even if you made these real copies, directly assigning nested structures is still a state mutation. – Brian Thompson Sep 16 '21 at 16:11
  • It's difficult to do an explanation justice in comments. I would recommend reading through the answer in the duplicate I linked. – Brian Thompson Sep 16 '21 at 16:12
  • So to use something like lodash's deepCopy would be the best way to go? Wil check the link out, thanks for the input! – mpmcintyre Sep 16 '21 at 16:16
  • 1
    Yes, I believe so. I have not used lodash, but have seen recommendations for that method before and my understanding is it will do what is required here. – Brian Thompson Sep 16 '21 at 16:17
  • Thank you very much for pointing that out :) I highly appreciate it! – mpmcintyre Sep 16 '21 at 16:25
  • 1
    No, you don't want to blanket deep copy/clone the ***entire*** state object as this would potentially lead to unnecessary rerenders. You must shallow copy into new object/array references any nested state that is *actually* being updated, the rest that isn't being updated can be forwarded to the new state object. Please also don't advocate using `JSON` to serialize/deserialize your production objects *just* to copy them. – Drew Reese Sep 16 '21 at 16:32
  • @Drew Would it be ideal to rather create multiple useState hooks for each desired state (e.g. one for items and one for hasMore) and ensure they do not unnecessarily update using useRef? – mpmcintyre Sep 16 '21 at 16:35
  • 1
    @DrewReese if the root state object is already a new object reference through spread syntax or any other methd, React will re-render regardless of if the nested array/objects have changed. So would a blanket deep-clone really cause extra renders? My understanding is no. Not necessarily arguing that I think its a good practice, but am wondering if it would ever make an observable difference – Brian Thompson Sep 16 '21 at 16:36
  • 1
    Take a look at the [immutable update patterns](https://redux.js.org/usage/structuring-reducers/immutable-update-patterns) to see a better explanation. – Drew Reese Sep 16 '21 at 16:37
  • @BrianThompson I said *potentially*... as in, if chunks of the nested state are passed to children.... if the nested references that didn't change value still have a reference that didn't change then React's reconciliation process can help bail out of unnecessary rerenders. But if you always create new references no matter what, even if the value is the same, then a rerender will occur regardless. You just make more work for React. – Drew Reese Sep 16 '21 at 16:39
  • Ok, yes the children re-renders is definitely a possibility. I read your comment as applying to the component holding the state. Thanks for the clarification. – Brian Thompson Sep 16 '21 at 16:40
  • Yes, I probably could/should have quantified that a bit more clearly. – Drew Reese Sep 16 '21 at 16:42
-1

you can achieve this easily with the bellow code:

const newOrganisations = { ...organisations };
newOrganisations.items[1].isInternal = true;
setOrganisations(newOrganisations);

and you can check out the code on the following code sandebox:

https://codesandbox.io/s/adoring-fermat-tqt1q?file=/src/App.js:349-484

Lakhdar
  • 377
  • 2
  • 8
  • This silently mutates state. I say silently because it only mutates deeply nested parts of state and React will re-render. However it is still an anti-pattern. – Brian Thompson Sep 16 '21 at 15:59
  • okay thanks with regard to targeting the organisation to update - you put .items[1]. However, i don't have the array index of the organisation to update, only its ID - which is in the item object itself (as in my question above). So, how do i target it by ID? – Paul Sep 16 '21 at 18:08
  • don't know about silent mutation that you are talking about, do you mean he should spread every array and object ? he can do that if this didn't work about the question of mutating by id, you do it as follows: const newOrganisations = { ...organisations }; const searchId = '3'; const foundIndex = newOrganisations.items.findIndex(item => item.id === searchId); if(foundIndex !== -1) { newOrganisations.items[foundIndex].isInternal = true; setOrganisations(newOrganisations); } – Lakhdar Sep 16 '21 at 19:39