Hello

I'm Alejandro

A Web Engineer from Málaga, Spain based in London, UK

How to break the rules, with conditional hooks

If you have been working with hooks for a while now, you probably know there are some rules around it. One of them is, hooks cannot be conditional or called inside loops, neither inside nested functions. They need to be on the top level function scope.

In this post, I'm going to focus on how you can apply a pattern that will let you use hooks or rather data generated by custom hooks conditionally.

As any other solution, it comes from a problem.

And the problem I had is probably a really common one:

You have to display images on your website, but they come from a source where the image is not stored, therefore, that source doesn't know the base url (or CDN url). But your website knows, and that value is on your Context (a React Context of course).

Because we want data from the Context, and it seems like this is something we are going to use a lot, you write a custom hook.

import { useContext } from 'react'
import ConfigContext from '../context/config'

function useImageUrl(relativeImageUrl: string): string {
  const config = useContext(ConfigContext)

  return `${config.baseImageUrl}${relativeImageUrl}`
}

Good enough. Right?

Then, you start using it.

import React from 'react'
import useImageUrl from '../hooks/useImageUrl'

function Home(): JSX.Element {
  return (
    <main>
      <h1>Home</h1>
      <img src={useImageUrl('/uploads/logo.png')} alt="" />
    </main>
  )
}

It plays well for a while, until you introduce a new feature, and now you are showing that image conditionally.

import React from 'react'
import useImageUrl from '../hooks/useImageUrl'

function Home({ showLogo: boolean }): JSX.Element {
  return (
    <main>
      <h1>Home</h1>
      {showLogo && <img src={useImageUrl('/uploads/logo.png')} alt="" />}
    </main>
  )
}

In this case, lots of unexpected things can happen, React will lose track of your hook and you will maybe get unexpected behaviour.

What can we do to fix it?

You could put them logoUrl in a variable and use it top level:

import React from 'react'
import useImageUrl from '../hooks/useImageUrl'

function Home({ showLogo: boolean }): JSX.Element {
  const logoUrl: string = useImageUrl('/uploads/logo.png')

  return (
    <main>
      <h1>Home</h1>
      {showLogo && <img src={logoUrl} alt="" />}
    </main>
  )
}

But, in this case:

  1. You are executing code when you probably don't need to.
  2. If the list of images you need to show becomes big, that can start to make your component unmanageable.
  3. This wont work for loops.
  4. When reading the code, if you don't see what the logoUrl value is, you have to go all the way up to your top level to find it.

What I came up with for those cases is something really simple.

  1. Get data in your custom hook.
  2. Return a function to modify the final value.

Here is how:

import { useContext } from 'react'
import ConfigContext from '../context/config'

function useImageUrl(): (relativeImageUrl: string) => string {
  const config = useContext(ConfigContext)

  return (relativeImageUrl: string) => {
    return `${config.baseImageUrl}${relativeImageUrl}`
  }
}

Then, from your component, all you have to do is useImageUrl on the top level, asign the value (our return function) to a variable, and use it anywhere you want.

import React from 'react'
import useImageUrl from '../hooks/useImageUrl'

function Home({ showLogo: boolean }): JSX.Element {
  const getAbsoluteUrl = useImageUrl()

  return (
    <main>
      <h1>Home</h1>
      {showLogo && <img src={getAbsoluteUrl('/uploads/logo.png')} alt="" />}
    </main>
  )
}
Share this post