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.