Photo by Florian Olivo on Unsplash
Derek Davis

Clean Up Your useEffect, But Not Too Much

The pitfalls of an implied cleanup function

lit up MacBook Pro keyboard
Photo by Nhu Nguyen on Unsplash

Like a lot of developers, I love to write code that is to the point and stripped of all the fluff. Something about getting code down to its smallest form is really satisfying. Although, at a certain point, conciseness and maintainability are tugging on opposite ends of the same strand.

Where this has particularly bitten me is with the cleanup function of a useEffect.

The Scenario

We start with a really simple useEffect.

useEffect(() => thing.register(), []);

Nothing special, right? Well, let's say later on we come back in here and decide brackets would look nicer, so it gets changed.

useEffect(() => {
  thing.register();
}, []);

Except... we have a problem now. These do not behave the same way. What we forgot is that thing.register() actually returns an unregister function that needs to be called in the effect cleanup. So what we should have done was this:

useEffect(() => {
  // now works the same as the implied return
  return thing.register();
}, []);

Conciseness vs. Maintainability

Let's consider this setup though.

Will we (or anyone else on our team) remember in 6 months that register() returns an unregister function that useEffect will call in its cleanup? We can only hope. The implied return in that first example makes it even more "magic."

Instead of hoping we remember that, let's instead create an intermediate variable to make it more clear.

useEffect(() => {
  const unregister = thing.register();
  return unregister;
}, []);

It's not as concise as the original, but I could come back after a long period of time and know exactly what that code is doing.

Summary

  • After refactoring, consider the impact to the maintainability of your code.
  • Make it clear when a useEffect has a cleanup function to avoid future defects.