loading...

Difficulties Encountered with React Hooks

Nick Taylor (he/him) on May 18, 2020

Photo by Dave Phillips on Unsplash Kent C. Dodds has a great post, 5 Tips to Help You Avoid React Hooks Pitfalls. There's lots of great advice in ... [Read Full]
markdown guide
 

Most of my work has been converting older class-based components to functional components, which means also using hooks. I think the syntax around passing useEffect which components to watch is a little weird. The distinction between [], empty, [prop] is harder to grasp than the class methods. Also useRef is a little tricky because I need to use the current property which took a little digging through the docs to realize. Overall I think they are a good way to keep functional components small and isolated.

 

For the most part I think hooks are a game changer, but there's one area I wish could be improved - using Redux's useDispatch inside useEffect. ESLint (or better yet, the rules of hooks) enforce you to pass the dispatch object as a dependency, resulting in an effect that fires on every render.

mapDispatchToProps to the rescue!

 

Does it work to get the dispatch method with usedispatch outside (possibly with a usecallback) and then you can pass dispatch in to the useEffect and that shouldn't then change every render. I think that's how you are meant to manage it

 

I guess it should work, but if you're going through so much trouble, mapDispatchToProps will probably be easier

I have a strong preference towards ditching the container/component split as it's hard for new react Devs to pick up and navigate the code. I'm not sure that passing in an extra prop is too much effort, but it is subjective

 

Totally agree. Encountered the same issue too and it's annoying to manually disable the linter for this line.

 

We were lucky to start a green field project with React around the Hooks beta, going live as it went to production. For me it's the reason we are using React now, it's so much cleaner and less complexity with hooks. We have found some interesting issues with async (just wrote an article on that) which we've found very elegant ways around, thanks again to hooks.

The biggest complexities for us have been the reduction of renders and understanding why they are happening. Plus a few cases of our model breaking the rules and calling update functions after some async when the component has become unmounted in the interim.

We've cleaned a lot of this up by doing a few wrapped hooks of our own that ensure nothing accidentally goes astray - keeps the code neater too.

 

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
 

For the most part I think hooks are great, and often make code reusability very simple, without the need to manually reimplement things into classes, or make tons of wrapper components.

The issue I have generally isn't with hooks itself, but the docs. They could often do with better and more in-depth explanations as to how they work. Some examples:

  • Why is it implied, even by the eslint rules, that all used variables must be included in the dependencies array? While it is sometimes necessary, it isn't always, as useEffect acts more like an 'onChange' listener, reacting when one of its dependencies are updated. The rest of the variables used inside it are still up-to-date when the function is run, but you may not want to trigger the effect every time one of the minor ones change.
  • Multiple usages of useState are updated individually, even if their setters are called consecutively. This can easily cause a lot of unexpected state inconsistencies. Ideally it'd be nice if these updates were batched, like with classic components, but at the very least, this should be made clearer to new users.
  • The importance/unimportance of memoizing functions with useCallback. I've seen arguments elsewhere online for not using it because it's 'slower'. However, whether it's slower or not often isn't the real issue - it's that the function must retain the same reference. When the reference changes, this causes its dependents to update; and that doesn't just mean re-renders, but can also mean re-invocations of things like useEffect which include it as a dependency.
 

It's not specifically documentation but if you want answers to all these questions, follow Dan abramov on Twitter and on his amazing blog overreacted.io. that really helped me understand at a deep level

 
 

How can I replicate the behavior of componentWillUnmount? It's important when the component subscribes to events when mounted.

 

Does returning a callback function for cleanup not work in your case?

useEffect(() => {
  // do something
  return () => yourCleanupHere()
})
 

Missed that feature, thanks for letting me know.

code of conduct - report abuse