Isolating Unused, Nil Expressions in React

A functional-first approach to programming discourages a number of typical JavaScript practices, including some practices that are even typical for React. In particular, React conventions nudge us towards having unused expressions, and to have expressions that are nil (undefined or null) with its useEffect hook. Let's consider how to isolate these instances.

Consider the following component:

let Sample = ({ id }: {id: string}): ReactElement => {
  let [thing, setThing] = useState()

  useEffect(() => {
    fetch(`/api/thing/${id}`)
      .then(setThing)

    return () => {
      // cleanup
    }
  }, [id])

  return (
    <>
      {thing}
    </>
  )
}

export default Sample

Here, useEffect is an unused expression. If you read the component from top to bottom, you have the function definition let Sample = whose binding is a function that returns a ReactElement. Then you have the function's parameter which includes the id prop - a bound variable. Next you have useState whose returned tuple is destructured and bound to thing and setThing. So far everything is bound to names - Sample, id, thing and setThing - with Sample accounting for the return statement at the end of the function.

The final piece in reading this function is useEffect. useEffect is different than the other elements in that it is an expression sitting in the middle of our component, and it's value is not bound to a variable. If it were bound to a name, it's value would be undefined because it's a void function. So the presence of useEffect in our component introduces both an unused expression (it's returned value is never used), and a nil expression (the value it returns is undefined or null).

While we don't want to be pedantic (to be sure, this may be pedantic to some no matter what), it is worth keeping the more commonly read files of our code in full alignment with our principles. Components are the most basic building block of a React codebase, and if we prefer functional-first programming then eliminating or isolating things that are not aligned with functional programming from components is, in fact, keeping clean code.

Isolating useEffect

The answer is to use a custom hook. In many cases isolating useEffect via a custom hook may occur for more practical purposes (for code reusability, for example), but we're talking about this from a strictly FP and style perspective - so it's worth doing it anyway. To do so, let's take our unused expression useEffect in its entirety and pull it from the component:

useEffect(() => {
  fetch(`/api/thing/${id}`)
    .then(setThing)

  return () => {
    // cleanup
  }
}, [id])

And drop it into a custom hook:

let useThing = (id?: string): Thing {
  let [thing, setThing] = useState<Thing>()

  useEffect(() => {
    fetch(`/api/thing/${id}`)
      .then(setThing)

    return () => {
      // cleanup
    }
  }, [id])

  return thing
}

export default useThing

Now, in the component, use the custom hook instead of useEffect:

let Sample = ({ id }: {id: string}): ReactElement => {
  let thing = useThing(id) // useThing is the custom hook

  return (
    <>
      {thing}
    </>
  )
}

export default Sample

Now that the Sample component's unused expressions have been isolated, everything has semantic purpose. Every name is used, and nothing is unnamed or unused.

However, thing can still possibly be nil if the API can return null. If we're avoiding nil expressions, we would update the setThing function in the custom hook to return empty for whatever data-type it expects in failure cases ({}, false, 0, [], etc).

Why Isolate Unused Expressions?

If you're using React, you likely have an idea of the benefits of functional programming. Isolating unused expressions is a means to address any concious effort to prefer a functional approach.

It also enhances readability. Any time an expression does not return to a named variable, it necessarily means that one has to dig into the body of the expression and determine what sort of side effects may be happening. Sure useEffect informs us to do some detective work in its name. But completely isolating unused expressions (side effects of any nature) allows for a component that reads meaninfully from top to bottom.