ElektrikSpark
ElektrikSpark

Reputation: 633

How can I change the state of arrays using hooks?

I don't know exactly what it is, but I have run into countless problems in trying to do the simplest state updates on arrays using hooks.

The only thing that I have found to work is using the useReducer to perform a single update on the array with putting dispatch on onClick handlers. In my current project, I am trying to update array state in a for loop nested in a function that runs on a form submit. I have tried many different solutions, and this is just one of my attempts.

  function sessionToState(session) {
    let formattedArray = []
    for (let i = 0; i < session.length; i++) {
      formattedArray.push({ url: session[i] })
      setLinksArray([...linksArray, formattedArray[i]])
    }
  }

  // --------------------------------------------------------

  return (
    <div>
      <form
        method="post"
        onSubmit={async e => {
          e.preventDefault()

          const session = await getURLs({ populate: true })

          sessionToState(session)

          await createGroup()

I was wondering if there are any big things that I am missing, or maybe some great tips and tricks on how to work with arrays using hooks. If any more information is needed don't hesitate to ask. Thanks.

Upvotes: 0

Views: 1828

Answers (3)

Aprillion
Aprillion

Reputation: 22324

I was wondering if there are any big things that I am missing

TLDR: setLinksArray does not update linksArray in the current render, but in the next render.


Assuming the variables are initialized as follows:

const [linksArray, setLinksArray] = useState([])

A hint is in the const keyword, linksArray is a constant within 1 render (and this fact wouldn't change with let, because it's just how useState works).

The idea of setLinksArray() is to make a different constant value in the next render.

So the for loop would be similar to:

setLinksArray([...[], session0])
setLinksArray([...[], session1])
setLinksArray([...[], session2])

and you would get linksArray = [session2] in the next render.

Best way to keep sane would be to call any setState function only once per state per render (you can have multiple states though), smallest change to your code:

function sessionToState(session) {
  let formattedArray = []
  for (let i = 0; i < session.length; i++) {
    formattedArray.push({ url: session[i] })
  }
  setLinksArray(formattedArray)
}

Furthermore, if you need to perform a side effect (like an API call) after all setState functions do their jobs, i.e. after the NEXT render, you would need useEffect:

useEffect(() => {
    ...do something with updated linksArray...
}, [linksArray])

For a deep dive, see https://overreacted.io/react-as-a-ui-runtime

Upvotes: 2

r g
r g

Reputation: 3901

Performance wise you shouldn't call setState every iteration. You should set state with final array.

const sessionToState = (session) => {
  setLinksArray(
   session.map(sessionItem => ({url: sessionItem}))
  );
}

... or if you want to keep old items too you should do it with function inside setState ...

const sessionToState = (session) => {
  setLinksArray(oldState => [
   ...oldState,
   ...session.map(sessionItem => ({url: sessionItem}))
  ]);
}

Upvotes: 2

jayarjo
jayarjo

Reputation: 16726

When invoking state setter from nested function calls you should use functional update form of setState. In your case it would be:

setLinksArray(linksArray => [...linksArray, formattedArray[i]])

It is not exactly clear what kind of problems you encounter, but the fix above will save you from unexpected state of linksArray.

Also this applies to any state, not only arrays.

Upvotes: 2

Related Questions