We're thinking about Hooks incorrectly

React's Hooks give us a cleaner way of writing our components. We don't need to write all that icky boilerplate of the JavaScript classes just to get access to React's lifecycles, we can just write a function component and throw a useEffect call in there...

Err, wait? Can we? Is that a fair analog?

Not really.

We can get into trouble if we start thinking about hooks like useEffect as a one-to-one parallel with componentDidMount. Sure, functionally specific uses of useEffect work similarly to componentDidMount, but that's just a one-off. It'll get confusing quickly if we don't change our thinking about how the hooks are supposed to be used.

It's becoming more well known within the community, but the paradigm is different with hooks. We're no longer thinking in terms of leveraging lifecycles to get things done. We're now thinking in terms of keyword functions (aka "hooks" like useEffect) being given to us which React will invoke alongside our state changes. It's a subtle difference so I'd like to try to clarify it a little bit.

Let's meet our cast of characters:

useEffect: the hook

useEffect(): the invocation of the hook

useEffect(fn,): the computation we want to do when the hook is invoked (typically, this is an arrow function)

useEffect(fn, [stateAttr]): the state with which this hook (and our fn) should be tied to

A function component, which has its own state, is invoked within the render phase where React will determine what has changed from time A to time B via reconciliation (the Fiber algo). This determination is done by creating a new representation of the state and comparing it against the current state. A hook, then, is invoked when a particular piece of state (or all/no pieces of state) changes. A Fiber representation of the "State of the World" for our application can be illustrated as:

{
  state: /* state of the world */,
  hooks: /* hooks present in the rendered component */
}

This is a simplified representation of a fiber node, and not accurate in a structural sense

If a hook is to be thought of as an inseparable sibling to the state you--as a developer--are tasked with telling React which piece(s) of the state it is tied to. For example:

useEffect(() => {
  doSomething()
})

This hook is now tied to every piece of application state and will be invoked alongside it post-render phase.


useEffect(() => {
  doSomething()
}, [])

This hook is now tied to the null of application state and will only be invoked on initial render. That might not make immediate sense based on what we discussed above. But it can be clarified a little by thinking about it like so:

[] ties the hook to the non-existent state, i.e. it will be invoked when the next state does not yet exist (during first render).


useEffect(() => {
  doSomething()
}, [price])

This hook is now tied to the price piece of the application state and will be invoked alongside its changes (when there is a new representation of it as a Fiber node).

The caveat with this is that we understand how React represents the state changes on its backend (in the source). If the state (or piece of state, in this example price) has not changed from render A to render B React won't create a new representation of it and hence, its "sibling hook" will not be invoked. To illustrate the point further here is a function component (PriceLabel) and its corresponding two Fiber nodes. One which represents the current state and one which represents the new state:

// PriceLabel Component
function PriceLabel () {
  const [price, setPrice] = useState(5)
  
  // In reality, you should never do this in useEffect
  useEffect(() => {
    setPrice(/* some computation */)
  }, [price])

  return (
   <div>{price}</div>
  )
}

// Current State
{
  state: { price: 5 },
  hooks: []
}

// New State after setPrice is called
{
  state: { price: 10 },
  hooks: [useEffect]
}

Again, this is a very simplified representation of a fiber node, and not accurate in a structural sense

In the above example, setPrice is what kicks-off the render phase and gives us the new representation of the state. Immediately following this phase our useEffect hook will be invoked because we told React,

Hey, when price changes in the next representation of state that you're creating, please run useEffect.

To briefly summarize what Ryan Florence pointed out with respect to the Hooks paradigm:

  • : run hook alongside all state changes (nothing provided, i.e. no array)
  • []: run hook alongside changes attributed to no (null) state (i.e. first render)
  • [pieceA]: run hook alongside pieceA changes

Topics