DEV Community

Discussion on: Difficulties Encountered with React Hooks

Collapse
 
wolfhoundjesse profile image
Jesse M. Holmes

I've been through several variations of fetching with useEffect, and I finally settled in on something without dependency arrays. The previous version lacked clarity when trying to fetch two or three things in a specific order, and performing state checks in the effect(s), e.g., if (!states), if (states && !zips) or something. Please excuse all of my contrived examples:

export const RandomDog = () => {
  const [src, setSrc] = useState()
  const [status, setStatus] = useState('idle')

  useEffect(() => {
    const fetchRandomDog = async () => {
      setStatus('loading')
      try {
        let res = await fetch("https://dog.ceo/api/breeds/image/random")
        setSrc(await res.json().message)
        setStatus('success')
      } catch (error) {
        setError(error)
        setStatus('failed')
      }
    }
    if (status === 'idle') {
      fetchRandomDog()
    }
  })

  return <img src={src} alt='A random dog' />
}

Of course, there are already three pieces of state moving in the same direction, and even with an object things will get pretty unruly, so it made sense to throw the state into a context to handle all the different endpoints:

export const RandomDog = () => {
  const state = useAppState()
  const dispatch = useAppDispatch()
  const { error, src, status } = state

  useEffect(() => {
    const fetchRandomDog = async () => {
      dispatch({ type: 'RandomDogRequested' })
      try {
        let res = await fetch("https://dog.ceo/api/breeds/image/random")
        let data = await res.json().message
        dispatch({ type: 'RandomDogReceived', payload: data })
      } catch (error) {
        dispatch({ type: 'RandomDogRequestFailed', payload: error })
      }
    }
    if (status === 'idle') {
      fetchRandomDog()
    }
  })

  return <img src={src} alt='A random dog' />
}

This worked fine, too. Even if I included dispatch and status in the dependency array, it was no problem. Unfortunately, there's no cleanup, and that should not go missing:

export const RandomDog = () => {
  const state = useAppState()
  const dispatch = useAppDispatch()
  const { error, src, status } = state

  useEffect(() => {
    const abortController = new AbortController()
    const fetchRandomDog = async () => {
      dispatch({ type: 'RandomDogRequested' })
      try {
        let res = await fetch("https://dog.ceo/api/breeds/image/random", { signal: abortController.signal })
        let data = await res.json().message
        dispatch({ type: 'RandomDogReceived', payload: data })
      } catch (error) {
        dispatch({ type: 'RandomDogRequestFailed', payload: error })
      }
    }
    if (status === 'idle') {
      fetchRandomDog()
    }

    return () => { 
      abortController.abort() 
    }
  })

  return <img src={src} alt='A random dog' />
}

Suddenly, I have an issue where as soon as the status is changed to loading, the fetch is cancelled. I added a check for the additional status, status === 'idle' || status === 'loading', and while my dog arrived unharmed, it wasn't without a cancelled request happening first. I need to get back to the code sandbox I was tinkering with when I was learning this bit.

The other time I run into issues with useEffect is when I am watching a dependency for a change, and the subsequent update causes a change to the dependency. I've stopped a runaway like this before by freezing the initial dependency with useRef, something along the lines of:

const initialAccounts = React.useRef(accounts)

useEffect(() => {
  if (someCondition) {
    updateAccounts([
      ...initialAccounts.current,
        someProp: updatedValue
    ])
  }
}, [updateAccounts]) // neither accounts nor initialAccounts is required here now