dontUseEffect: Write Cleaner, Faster React Code

dontUseEffect: Write Cleaner, Faster React Code

I was once called in to take over the most complex feature on a project that was nearing its deadline. The thing was an unmaintainable beast with >1000 lines of code in a single file. We had business requirements changing every week and, yes, it broke nearly every single day! Every fix or sub-feature I'd push would end up creating regression issues down the line.

At the core of it all was a dreadful pattern centered around useEffects that frankly still gives me PTSD. Sadly, the team that built this had long since moved on to another project or I'd have a sit-down with the developer who approved it.

Here's a simplified version of that logic,

Article content
This screenshot is the closest we can get to enjoying the controversial Material theme anymore :(

In this example, a new variable called importantParam had to be introduced, which the handleApi would pass to the API request as a param. Suddenly, handleApi was no longer firing when this state variable changed!

Not a problem! You'd add a useEffect that would then fire the API call when the importantApiParam changes like so,

Article content

Excellent! This works but you've done is added a lot of complexity to your component. You've introduced a lot of state that you need to manage and keep track of. This is a lot of cognitive overhead for a simple API call 🥲

What if, tomorrow, business says a new API param should also be passed to the API call? Now here’s where things get really messy!

You'd have to add another state variable, another useEffect to manage that state and another useEffect to fire the API call. This is a slippery slope that you don't want to be on.

I'd be remiss not to mention, if you're making API calls, consider creating a custom hook for it or using TanStack Query

Article content
Do check out Tanner Linsey's live presentations. His slides are even prettier than his logos!

🚩 There are situations where you need side effects or they're simply unavoidable but any component with more than 2-3 useEffect hooks is a sitting red flag. If you have a small atomic component like a button or a card, you probably don't need a useEffect to begin with.

What if I have two state variables that need to be in sync?

In most cases, if you're in a situation where you need 2 variables to be in sync with each other, you're probably framing your problem incorrectly to begin with.

However, you might find yourself in an unavoidable situation where you need to keep 2 state variables in sync, especially when the state variables are used in a complex calculation. Here are 2 approaches (I'll be using a very contrived example):

useNaive 🙅

Article content

useMemo 🤷

Article content

Less boilerplate and more of keeping things closer to where they're used. The person who will be tirelessly maintaining your code when you go on your annual "spiritual" trip to Bali will thank you for it.

You might find yourself in a situation where you need to update several state variables based on the previous state. Perhaps these state variables are loosely coupled and don't need to be in sync all the time, but they still update together in some predictable way. This is where useReducer comes in.

useReducer 🤔

Let's take a step back and consider what solution we're trying to design. Here's the naive approach visualized:

Article content
Updating mainVar triggers the useEffect, which in turn, updates the dependent variables

Despite the author's eye-pleasing visual rendition of the solution (or perhaps because of it), the problems here are painfully self-evident. This is a lot of boilerplate and a lot of cognitive (and performance) overhead for a simple problem. Things chained together like this are a nightmare to maintain and debug, especially if your component is already 1000+ lines of code long.

Let's take a step back and try to visualize an ideal solution. In an ideal world, we'd want to:

  1. 📦 Bundle all the state variables together in some way.
  2. ⚡ Reduce the number of state updates to only those that are necessary.

This is exactly what useReducer helps us do!

Article content

Here's a visual representation of our new consolidated solution. Isn't she a beauty?

Article content
This sort of refactor pays dividends at scale, especially across fast-growing frontend teams

This is a much cleaner solution that:

  1. 📦 Keeps all the logic in one place
  2. 🐞 Is easier to maintain and debug, and,
  3. ⚡ Is more performant than the naive approach.

With useReducer, you stop thinking in terms of “what changed” and start thinking in terms of “what action was taken.” That mindset shift alone makes complex state easier to debug and extend.

Truly, useReducer is probably the most underrated hook in React. It's a powerful tool that can help you manage complex state logic with ease. You probably wouldn't use it every day, but when you need it, it'll come in clutch for you.

To sum up, useEffect isn't the work of the devil but it also isn't the answer to every problem. It's a powerful tool, but it's easy to misuse. The next time you find yourself reaching for useEffect, take a step back and consider if there's a better way to solve your problem.

Quite often, there is!

If there's a pattern in React that gives you nightmares, let us know in the comments!

Mohammed Owais

Tech & Digital Director at PwC. Speaker and technology advisor. ex-SaaS CTO, ex-leader for UAE Microsoft communities.

2mo

If I can't have a list of patterns, then a list of anti-patterns would do. Sometimes junior devs need to see exactly what could go wrong so they can appreciate the problems the standards are solving!

To view or add a comment, sign in

Others also viewed

Explore topics