0

I have an onClick event on an input field called handleLinkChange that validates its content (a YouTube link) and if it's valid calls a function that extract its YouTube ids.

Within the onClick event I update the links state so the user sees if the link is valid and the extraction of ids is in progress (indicated by a loading wheel in the input field). At this point the state updates as desired in the DOM. However, when I arrive in the getIdsFromLink function after setting the state, the links state is still at the initial value. This makes it impossible to alter the links, for example to replace the loading indicator of the link with a checkmark when the ids have been parsed.

  // Links state
  const [links, setLinks] = useState([{ link: '', state: IS_VALID, errorMessage: '' }])

  // onChange event handler
  const handleLinkChange = (event, index) => {
    const clonedLinks = [...links]

    let value = decodeURI(event.target.value.trim())

    const { passesRegex, validityMessage } = checkLinkValidity(value)

    clonedLinks[index] = {
      link: value,
      state: passesRegex ? IS_VALIDATING_SERVER_SIDE : IS_ERROR,
      errorMessage: validityMessage,
    }

    setLinks(clonedLinks)

    if (clonedLinks[index] !== '' && passesRegex) {
      getIdsFromLink(clonedLinks[index].link, index)
    }
  }

  // Parser
  const getIdsFromLink = (link, index) => {
    console.log('links state', links) // initial state

    socket.emit('getIds', link)

    socket.on('idsParsed', newIds => {
      console.log('links state after parse', links) // initial state
    })
  }

  // Shortened return
  return (
      links.map((link, index) => (
        <Input
          value={link.link} 
          onChange={event => handleLinkChange(event, index)}
        />

      {link.link && (
        <FontAwesomeIcon
          icon={link.state === IS_VALID ? faCheck : link.state === IS_ERROR ? faTimes : faSpinner}
        />
      )}
    )
  ))

I know that states are asynchronous and I also tried watching for state changes with useEffect, but I'm unable to refactor my code in that way and I have another state object that heavily depends on the links state, so I gave up on that one.

Even when I try to use flushSync to update the state synchronously, I have the same effect.

I very much appreciate your help! Thanks!

GoYoshi
  • 253
  • 1
  • 3
  • 17
  • 1
    `clonedLinks` represents your current state, so you can pass that to `getIdsFromLink()`. If you're planning to update your state using a second `setLinks()` call in `getIdsFromLink`, then you should remove the first `setLinks()` call as that will be overwritten by the second `setLinks()` call you do. You can also access the current state value using `setLinks(currLinks => ...)` – Nick Parsons Dec 10 '22 at 12:51
  • Does this answer your question? [The useState set method is not reflecting a change immediately](https://stackoverflow.com/questions/54069253/the-usestate-set-method-is-not-reflecting-a-change-immediately) – Konrad Dec 10 '22 at 12:51
  • @NickParsons The reason why I don't pass `clonedLinks` to `getIdsFromLink()` is that during the parsing of the ids (which is a socket call), the user could add more links and therefore, when the parsing of a link is complete, its `clonedLinks` may not reflect the newest changes of the links state. Also I need `setLinks()` before the parsing so the user sees the loading indicator next to the input. – GoYoshi Dec 10 '22 at 12:59
  • @NickParsons Actually using `setLinks` with the function parameter makes it possible to access the current state, so the problem I described in the above comment is solved! So now I change my `links` object in the function parameter and return the new object at the end. I just had to make sure to make a clone of the current state object of the function parameter to force a rerender of the state. – GoYoshi Dec 10 '22 at 14:00

1 Answers1

1

I'm gonna answer my question as I figured it out thanks to Nick in the comments.

Basically I kept the handleLinkChange function unchanged. But in the getIdsFromLink function, when the link has been parsed from the server - that's where I have to update the state - I used setLinks with a function as parameter. The function receives an argument which resembles the current state. All I had to do is to make a copy of the object (for the rerender to work), then make my changes to it and finally return the new object.

  const getIdsFromLink = (link, index) => {
    socket.emit('getIds', link)

    socket.once('idsParsed', ids => {
      // Function parameter to get current state
      setLinks(_currentLinks => {
        const currentLinks = [..._currentLinks] // copy to force rerender

        currentLinks[index].state = IS_VALID
        currentLinks[index].ids = ids
        
        console.log('finished currentLinks', currentLinks) // newest state

        return currentLinks
      })
    })
  }
GoYoshi
  • 253
  • 1
  • 3
  • 17