Search code examples
reactjstypescriptreact-router-domreact-hook-form

Data not updating with useParams for react-hook-form


Not entirely sure how to title this, but here's my issue: I'm making a character sheet of sorts, and you can swap between your characters by clicking on them in the nav bar. When selecting a different character, my useEffect for my watch doesn't seem like it wants to move over with it. I have to use local storage as this is for a course I'm taking, and we can only deal with the front end, currently. I've been trying to deal with this bug for long enough that I'm now reaching out to see if anybody else can give me some insight.

Here's an example of how the code looks (just the name for example):

const { characterID } = useParams()

// Sore the characterID in a useRef. It's used for a condition later, since react-hook-form was overwriting data of the character you move from, to the one you move to.
const currentID = useRef<string | undefined>(characterID)

const {watch, reset} = useForm()

useEffect(()=>{

    currentID.current = characterID

    let values = {
        characterName: characterInfo.name
    }

    reset({ ...values })

}, [characterID])


// The useEffect to edit data in localStorage based on watch.
useEffect(()=>{
    const subscription = watch((data) => {
        if ( characterID === currentID.current ) {
            localStorage.setItem(`CharacterName${characterID}`, JSON.stringify(data.characterName))
        }
    })
    return () => subscription.unsubscribe()
}, [watch])

My register for characterName is just on a simple input.

I've used console.log to confirm that it's only going inside my if statement if it's the original character selected first, or if it was refreshed even though the statement is true. I can provide more details if needed, but I just don't know anymore.

(Edit to add: I have that condition in there because the data for the character you select next overwrites the previous one. I assume this happens because it sees the data changes, so it saves it all before it has a chance to update the characterID from the params.)


Solution

  • It looks like the second useEffect is just missing some dependencies. Assuming watch is a stable function reference provided by useForm then the problem is that the useEffect callback is called only once after the initial render cycle, instantiates a watch callback function that closes over the characterID value from that render, then the effect never runs again. The watch callback ends up having a stale Javascript closure over the characterID variable value.

    To fix this ensure you have added all external (to the hook) dependencies to the useEffect hook's dependency array.

    useEffect(() => {
      const subscription = watch((data) => {
        if (characterID === currentID.current) {
          localStorage.setItem(
            `CharacterName${characterID}`,
            JSON.stringify(data.characterName)
          );
        }
      });
    
      return subscription.unsubscribe;
    }, [characterID, watch]);
    

    Now when the characterID route path parameter value changes, the effect will run again, the cleanup function from the previous call will unsubscribe the previous watcher, and a new one will be instantiated with a closure over the current characterID value.