React useEffect and objects as dependency - 4 approaches to avoid unnecessary executions
Tags: react, javascript, webdev, frontend
Published: 2024-03-22 13:34:00

---
description: React’s useEffect hook can lead to tricky situations. If you’re not careful it can cause unnecessary executions of the effect or even infinite re-renders. Especially when using objects as dependencies. Here are 4 approaches that avoid unnecessary executions.
---

React’s `useEffect` hook can lead to tricky situations. If you’re not careful it can cause unnecessary executions of the effect or even infinite re-renders. Especially when using objects as dependencies.

In this blog post, you'll see four different approaches to using an object as a `useEffect` dependency. All have their pros and cons, from being simple yet ineffective, to ugly yet efficient.

{% embed https://youtu.be/I-7IkX5CG60 %}

## Table Of Contents

1. [The problem](#the-problem)
2. [Approach 1: Spread object values](#approach-1-spread-object-values)
3. [Approach 2: Manually pass each value](#approach-2-manually-pass-each-value)
4. [Approach 3: Third-party useDeepCompareEffect hook](#approach-3-thirdparty-usedeepcompareeffect-hook)
5. [Approach 4: Stringifying the object](#approach-4-stringifying-the-object)
6. [Summary](#summary)

## The problem

Let me walk you through a simplified example I encountered while working on a coding task. 

```typescript
function useGetProducts(filters: Record<string, string>) {
  useEffect(() => {
    syncFilters(filters);
  }, [filters]);

  // ... rest of the hook
}

function ProductList() {
  const products = useGetProducts({ brand: "Nike", color: "red" });
  return (
    <div>
      {products.map((p) => (
        <ProductCard key={p.id} {...p} />
      ))}
    </div>
  );
}
```

Now, at first glance, this looks all fine. However, using the params object as a dependency of the `useEffect` is problematic. 

Can you see it?

Have a look at how we pass the `filters` object from the `ProductList` component to the`useGetProducts` hook. 

```typescript
function ProductList() {
  const products = useGetProducts({ brand: "Nike", color: "red" });
  ...
```

During each render, this object is created from scratch. The `useEffect` internally compares the dependencies by reference. And since the reference to the `filters` object is different for each render, the effect would be run with every render as well.

![The useEffect runs on every render](no-optimization::https://media.graphassets.com/aSOWSAFmTcSLEYOxEt84)

This is less than ideal, but not that easy to spot. 

## Approach 1: Spread object values

A simple solution is to spread all values of this object as dependencies. 

```typescript
function useGetProducts(filters: Record<string, string>) {
  useEffect(() => {
    syncFilters(filters);
  }, [...Object.values(filters)]);

  // ... rest of the hook
}
```

Technically, this isn't problematic, because the effect is run whenever one of the values of the params object changes. But we get a lint warning, and would have to disable it to commit our code.

![Missing dependency warning](https://media.graphassets.com/6sUHbFoRyZGitizkbA0A)

While disabling warnings isn't necessarily a huge problem, this could lead us to forget about a missing dependency later if we, for example, add another filter. This could then lead to a hard-to-find bug. 

## Approach 2: Manually pass each value

Another approach is to destructure each filter and pass it as a separate dependency. 

```typescript
function useGetProducts(filters: Record<string, string>) {
  const { brand, color } = filters;
  useEffect(() => {
    syncFilters({ brand, color });
  }, [brand, color]);

  // ... rest of the hook
}
```

But this is a bit tedious, and we might forget to add a new parameter if new filters are introduced. Also, in our case, this doesn’t really work as the `filters` object could contain any string as key.

## Approach 3: Third-party useDeepCompareEffect hook

Another approach is using [this useDeepCompareEffect](https://github.com/kentcdodds/use-deep-compare-effect) created by Kent C. Dodds. This hook is similar to the native `useEffect`, but instead of comparing the dependencies by reference, it makes a deep comparison of all values inside an object. 

Let's give it a try. First, we install the dependency.

```typescript
npm i use-deep-compare-effect
```

Then, we replace the useEffect with a new useDeepCompareEffect hook. We can now simply pass the `filters` object as a dependency, and the effect won't be run on every render anymore.

```typescript
function useGetProducts(filters: Record<string, string>) {
  useDeepCompareEffect(() => {
    syncFilters(filters);
  }, [filters]);

  // ... rest of the hook
}
```

The problem with this hook? When we remove a required dependency like the params object, we don't get a lint warning about missing dependencies. 

![No missing dependency warning with useDeepCompareEffect](https://media.graphassets.com/KTzyTa8nSQGK5QlQPPS7)

So, this isn't really better than our destructuring approach before.

## Approach 4: Stringifying the object

A final approach that I saw Dan Abramov recommend somewhere is stringifying the object and parsing it again inside the `useEffect`. 

```typescript
function useGetProducts(filters: Record<string, string>) {
  const json = JSON.stringify(filters);
  useEffect(() => {
    const filters = JSON.parse(json);
    syncFilters(filters);
  }, [json]);

  // ... rest of the hook
}
```

This works well with small and not too deeply nested objects that don't contain function values. Honestly, it doesn't look that great, but combines all of the advantages: It decreases the risk of forgetting to add filters and the risk of forgetting to add a dependency in the future. All while keeping the ESLint check intact. 

The main problem with this approach is that we lose the type of the `filters` object inside the `useEffect`.

![Type of filters is any](https://media.graphassets.com/QRhFc6FiTJOQQujmdVol)

So inside the `useEffect` we need to manually assign the type again.

```typescript
function useGetProducts(filters: Record<string, string>) {
  const json = JSON.stringify(filters);
  useEffect(() => {
    const filters: Record<string, string> = JSON.parse(json);
    syncFilters(filters);
  }, [json]);

  // ... rest of the hook
}
```

## Summary

A `useEffect` in React can be tricky. Especially when you need to use an object as a dependency. In this blog post we covered 4 techniques to avoid unnecessary executions of the effect by

- spreading the object values
- manually adding the values
- using the third-party useDeepCompareEffect
- stringifying the object.

All approaches have their pros and cons and it depends on the situation which one makes most sense for you.

[![The React Job Simulator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8bupb5hadajo1ok905li.png)](https://profy.dev)