useCallback and dependency abuse with React hooks
Tags: react, hooks, patterns, architecture
Published: 2019-10-04 09:23:22

React hooks make some people [frustrated](https://blog.logrocket.com/frustrations-with-react-hooks/) and not without a reason.

With hooks, some patterns you were used to, simply stop working. In this post, I'd like to suggest a slightly different approach to function handling in React components than it's recommended in most articles/tutorials.

Imagine a simplified `QuizPage` component that looks like the following:

```js
export default function QuizPage(props) {
  let {quiz} = props

  let [done, setDone] = useState(false) // finished or not finished
  let [selected, setSelected] = useState({}) // selected options
 
  return <Quiz {...props}
               done={done} setDone={setDone}
               chosen={chosen} setChosen={setChosen}/>
}
```

I delegated rendering to another `Quiz` component to simplify explanation.
The `QuizPage` deals only with state/logic while `Quiz` takes the rendering part which is a reasonable separation.
 
Btw. you would have the above code structure with [NextJS](https://nextjs.org) React framework, which I highly recommend.

Now say we want to store the quiz result in a session storage. How would we do that?

We start naively:

```js
export default function QuizPage(props) {
  let {quiz} = props

  let [done, setDone] = useState(false)
  let [selected, setSelected] = useState({})

  // +++
  function loadFromSession(slug) {
    let data = sessionStorage.getItem("progress:" + slug)
    if (data) {
      let obj = JSON.parse(data)
      setDone(obj.done)
      setSelected(obj.selected)
    }
  }
   
  // +++  
  function saveToSession(slug) {
    let json = JSON.stringify({
      done, tabIndex, chosen
    })
    sessionStorage.setItem("progress:" + slug, json)  
  }

  // +++
  useEffect(() => {
    loadFromSession(quiz.slug)
    return () => {
      saveToSession(quiz.slug)
    }
  }, [quiz.slug])
 
  return <Quiz {...props}
               done={done} setDone={setDone}
               selected={selected} setSelected={setSelected}/>
}
```

*The above is clearly simplified as I don't check for `window.sessionStorage`, etc. It should be good enough for demonstration purposes, though.*

Our code doesn't work as soon as we have two quizzes. 

`useEffect` captures the "wrong" `saveToSession`. The `[quiz.slug]` part prevents `useEffect` from loading and saving data on each render, which we want. But, at the same time, it prevents `useEffect` from capturing the newer *versions* of functions, which introduces a bug.

At this point, a newcomer starts to dispair and then searches the internet. She sees the `useCallback` function being glorified in many tutorials. So she changes her code to something like this:

```js
export default function QuizPage(props) {
  let {quiz} = props

  let [done, setDone] = useState(false)
  let [selected, setSelected] = useState({})

  // ***
  let loadFromSession = useCallback(slug => {
    let data = sessionStorage.getItem("progress:" + slug)
    if (data) {
      let obj = JSON.parse(data)
      setDone(obj.done)
      setSelected(obj.selected)
    }
  }, [...deps...])

  // ***
  let saveToSession = useCallback(slug => {
    let json = JSON.stringify({
      done, tabIndex, chosen
    })
    sessionStorage.setItem("progress:" + slug, json)  
  }
  }, [...deps...])

  useEffect(() => {
    loadFromSession(quiz.slug)
    return () => {
      saveToSession(quiz.slug)
    }
  // ***
  }, [quiz.slug, loadFromSession, saveToSession]) 
  
 
  return <Quiz {...props}
               done={done} setDone={setDone}
               selected={selected} setSelected={setSelected}/>
}
```

Now it doesn't look like **anything familiar** anymore. While the code evolves, the pain of dependency juggling becomes more and more prominent.

It's easy to make a mistake and spend hours debugging the bugs with asynchronicity and race conditions. The code is just too complex for what it should be for such a trivial task! 

But what if we take another approach to "unfreezing" `useEffect`. What if, instead of dancing around reactive dependencies, we emulate OOP approach? How it would look like? 

```js
export default function QuizPage(props) {
  let {quiz} = props

  let [done, setDone] = useState(false)
  let [selected, setSelected] = useState({})

  let self = useRef()  

  // +++
  useEffect(() => {
    self.current = { 
      loadFromSession(slug) {
        let data = sessionStorage.getItem("progress:" + slug)
        if (data) {
          let obj = JSON.parse(data)
          setDone(obj.done)
          setSelected(obj.selected)
        }
      },

      saveToSession(slug) {
        let json = JSON.stringify({
            done, tabIndex, chosen
        })
        sessionStorage.setItem("progress:" + slug, json)  
      })
    })
  })

  useEffect(() => {
    self.current.loadFromSession(quiz.slug)
    return () => {
      self.current.saveToSession(quiz.slug)
    }
  }, [quiz.slug]) // !!!
 
  return <Quiz {...props}
               done={done} setDone={setDone}
               selected={selected} setSelected={setSelected}/>
}
```

Whoops! Notice how all the reactive mess is gone and only `[quiz.slug]` which concerns the real **business logic** is left. 

Right tool for the right job!

I very much prefer this version to the previous and honestly I think `useCallback` is overused. And one of the reasons it's overused is that memoization is overrated in React community. But that is a story for another day.

I think you cross the line of readability/reasonability when your start passing functions as *dependency* arguments of `useEffect` and co. And that is something some influential bloggers recommend which I'm slightly worried about. 

P.S

Diff highlighting is broken at **dev.to** so I used code comments instead.