Fantastic closures and how to find them in React
Tags: react, webdev, javascript, tutorial
Published: 2023-08-17 09:40:53


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vwsol5i2i6nqkso7ww2r.png)

This article is [available in video format](https://youtu.be/AhAGA5LUxek). Fewer details, but nice animations and voice instead of letters.

Also, this article is part of the ["Advanced React"](https://www.advanced-react.com) book. If you like it, you might like the book as well 😉

## Table of content

- [The problem](#part1)
- [JavaScript, scope, and closures](#part2)
- [The stale closure problem](#part3)
- [Stale closures in React: useCallback](#part4)
- [Stale closures in React: Refs](#part5)
- [Stale closures in React: React.memo](#part6)
- [Escaping the closure trap with Refs](#part7)

--- 

Closures in JavaScript must be one of the most terrifying features of the language. Even the omniscient ChatGPT will tell you that. It’s also probably one of the most hidden language concepts. We use it every time we write any React code, most of the time without even realizing it. But there is no getting away from them in the end: if we want to write complex and performant React apps, we have to know closures.

So let’s dive into yet another code mystery, and in the process learn:

- What closures are, how they appear, and why we need them.
- What a stale closure is, and why they occur.
- What the common scenarios in React are that cause stale closures, and how to fight them.

Warning: if you've never dealt with closures in React, this article might make your brain explode. Make sure to have enough chocolate with you to stimulate brain cells while you're reading this.

<h2 id="part1">
  <a href="#part1">The problem</a>
</h2>

Imagine you're implementing a form with a few input fields. One of the fields is a very heavy component from some external library. You don't have access to its internals, so you can't fix its performance problems. But you really need it in your form, so you decide to wrap it in `React.memo`, to minimize its re-renders when the state in your form changes. Something like this:

```tsx
const HeavyComponentMemo = React.memo(HeavyComponent);

const Form = () => {
  const [value, setValue] = useState();

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <HeavyComponentMemo />
    </>
  );
};
```

So far, so good. This Heavy component accepts just one string prop, let's say `title`, and an `onClick` callback. This one is triggered when you click a "done" button inside that component. And you want to submit your form data when this click happens. Also easy enough: just pass the `title` and `onClick` props to it.

```tsx
const HeavyComponentMemo = React.memo(HeavyComponent);

const Form = () => {
  const [value, setValue] = useState();

  const onClick = () => {
    // submit our form data here
    console.log(value);
  };

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <HeavyComponentMemo
        title="Welcome to the form"
        onClick={onClick}
      />
    </>
  );
};
```

And now you'll face a dilemma. As we know, every prop on a component wrapped in `React.memo` needs to be either a primitive value or persistent between re-renders. Otherwise, memoization won't work. So technically, we need to wrap our `onClick` in `useCallback`:

```tsx
const onClick = useCallback(() => {
  // submit data here
}, []);
```

But also, we know that the `useCallback` hook should have all dependencies declared in its dependencies array. So if we want to submit our form data inside, we have to declare that data as a dependency:

```tsx
const onClick = useCallback(() => {
  // submit data here
  console.log(value);

  // adding value to the dependency
}, [value]);
```

And here's the dilemma: even though our `onClick` is memoized, it still changes every time someone types in our input. So our performance optimization is useless.

Okay, fair enough, let's look for other solutions. `React.memo` has a thing called [comparison function](https://react.dev/reference/react/memo#specifying-a-custom-comparison-function). It allows us more granular control over props comparison in `React.memo`. Normally, React compares all "before" props with all "after" props by itself. If we provide this function, it will rely on its return result instead. If it returns `true`, then React will know that props are the same, and the component shouldn't be re-rendered. Sounds exactly what we need.

We only have one prop that we care about updating there, our `title`, so it's not going to be that complicated:

```tsx
const HeavyComponentMemo = React.memo(
  HeavyComponent,
  (before, after) => {
    return before.title === after.title;
  },
);
```

The code for the entire form will then look something like this:

```tsx
const HeavyComponentMemo = React.memo(
  HeavyComponent,
  (before, after) => {
    return before.title === after.title;
  },
);

const Form = () => {
  const [value, setValue] = useState();

  const onClick = () => {
    // submit our form data here
    console.log(value);
  };

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <HeavyComponentMemo
        title="Welcome to the form"
        onClick={onClick}
      />
    </>
  );
};
```

And it worked! We type something in the input, the heavy component doesn't re-render, and performance doesn't suffer.

Except for one tiny problem: it doesn't actually work. If you type something in the input and then press that button, the `value` that we log in `onClick` is `undefined`. But it can't be undefined, the input works as expected, and if I add `console.log` outside of `onClick` it logs it correctly. Just not inside `onClick`.

```tsx
// those one logs it correctly
console.log(value);

const onClick = () => {
  // this is always undefined
  console.log(value);
};
```

You can play around with the full example here:

{% embed https://codesandbox.io/embed/example1-925swq?codemirror=1&hidedevtools=0&view=preview %}

What's going on?

This is known as the "stale closure" problem. And in order to fix it, we first need to dig a bit into probably the most feared topic in JavaScript: closures and how they work.

<h2 id="part2">
  <a href="#part2">JavaScript, scope, and closures</a>
</h2>

Let's start with functions and variables. What happens when we declare a function in JavaScript, either via normal declaration or via arrow function?

```tsx
function something() {
  //
}
const something = () => {};
```

By doing that, we created a _local scope:_ an area in our code where variables declared inside won't be visible from the outside.

```tsx
const something = () => {
  const value = 'text';
};

console.log(value); // not going to work, "value" is local to "something" function
```

This happens every time we create a function. A function created inside another function will have its own local scope, invisible to the function outside.

```tsx
const something = () => {
  const inside = () => {
    const value = 'text';
  };

  console.log(value); // not going to work, "value" is local to "inside" function
};
```

In the opposite direction, however, it's an open road. The inner-most function will "see" all the variables declared outside.

```tsx
const something = () => {
  const value = 'text';

  const inside = () => {
    // perfectly fine, value is available here
    console.log(value);
  };
};
```

This is achieved by creating what is known as "closure". The function inside "closes" over all the data from the outside. It's essentially a snapshot of all the "outside" data frozen in time stored separately in memory.

If instead of creating that `value` inside the `something` function, I pass it as an argument and return the `inside` function:

```tsx
const something = (value) => {
  const inside = () => {
    // perfectly fine, value is available here
    console.log(value);
  };

  return inside;
};
```

We'll get this behavior:

```tsx
const first = something('first');
const second = something('second');

first(); // logs "first"
second(); // logs "second"
```

We call our `something` function with the value "first" and assign the result to a variable. The result is a reference to a function declared inside. A closure is formed. From now on, as long as the `first` variable that holds that reference exists, the value "first" that we passed to it is frozen, and the `inside` function will have access to it.

The same story with the second call: we pass a different value, a closure is formed, and the function returned will forever have access to that variable.

This is true for any variable declared locally inside the `something` function:

```tsx
const something = (value) => {
  const r = Math.random();

  const inside = () => {
    // ...
  };

  return inside;
};

const first = something('first');
const second = something('second');

first(); // logs random number
second(); // logs another random number
```

It's like taking a photograph of some dynamic scene: as soon as you press the button, the entire scene is "frozen" in the picture forever. The next press of the button will not change anything in the previously taken picture.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3rvi4ymuy3l4her7avlf.png)


In React, we're creating closures all the time without even realizing it. Every single callback function declared inside a component is a closure:

```tsx
const Component = () => {
  const onClick = () => {
    // closure!
  };

  return <button onClick={onClick} />;
};
```

Everything in `useEffect` or `useCallback` hook is a closure:

```tsx
const Component = () => {
  const onClick = useCallback(() => {
    // closure!
  });

  useEffect(() => {
    // closure!
  });
};
```

All of them will have access to state, props, and local variables declared in the component:

```tsx
const Component = () => {
  const [state, setState] = useState();

  const onClick = useCallback(() => {
    // perfectly fine
    console.log(state);
  });

  useEffect(() => {
    // perfectly fine
    console.log(state);
  });
};
```

Every single function inside a component is a closure since a component itself is just a function.

<h2 id="part3">
  <a href="#part3">The stale closure problem</a>
</h2>

But all of the above, although slightly unusual if you're coming from a language that doesn't have closures, is still relatively straightforward. You create a few functions a few times, and it becomes natural. It's even unnecessary to understand the concept of "closure" to write apps in React for years.

So what is the problem, then? Why are closures one of the most terrifying things in JavaScript and a source of pain for so many developers?

It's because closures live for as long as a reference to the function that caused them exists. And the reference to a function is just a value that can be assigned to anything. Let's twist our brains a bit. Here's our function from above, that returns a perfectly innocent closure:

```tsx
const something = (value) => {
  const inside = () => {
    console.log(value);
  };

  return inside;
};
```

But the `inside` function is re-created there with every `something` call. What will happen if I decide to fight it and cache it? Something like this:

```tsx
const cache = {};

const something = (value) => {
  if (!cache.current) {
    cache.current = () => {
      console.log(value);
    };
  }

  return cache.current;
};
```

On the surface, the code seems harmless. We just created an external variable named `cache` and assigned our inside function to the `cache.current` property. Now, instead of this function being re-created every time, we just return the already saved value.

However, if we try to call it a few times, we'll see a weird thing:

```tsx
const first = something('first');
const second = something('second');
const third = something('third');

first(); // logs "first"
second(); // logs "first"
third(); // logs "first"
```

No matter how many times we call the `something` function with different arguments, the logged value is always the first one!

We just created what is known as the "stale closure". Every closure is frozen at the point when it's created. When we first called the `something` function, we created a closure that has "first" in the `value` variable. And then, we saved it in an object that sits outside of the `something` function.

When we call the `something` function the next time, instead of creating a new function with a new closure, we return the one that we created before. The one that was frozen with the "first" variable forever.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9h0qlk2bm55k0jaarod.png)


In order to fix this behavior, we'd want to re-create the function and its closure every time the `value` changes. Something like this:

```tsx
const cache = {};
let prevValue;

const something = (value) => {
  // check whether the value has changed
  if (!cache.current || value !== prevValue) {
    cache.current = () => {
      console.log(value);
    };
  }

  // refresh it
  prevValue = value;
  return cache.current;
};
```

Save the value in a variable so that we can compare the next value with the previous one. And then refresh the `cache.current` closure if the variable has changed.

Now it will be logging variables correctly, and if we compare functions with the same value, that comparison will return `true`:

```tsx
const first = something('first');
const anotherFirst = something('first');
const second = something('second');

first(); // logs "first"
second(); // logs "second"
console.log(first === anotherFirst); // will be true
```

Play around with the code here:

{% embed https://codesandbox.io/embed/example2-xx4xq3?codemirror=1&hidedevtools=0&view=split %}

<h2 id="part4">
  <a href="#part4">Stale closures in React: useCallback</a>
</h2>

We just implemented almost exactly what the `useCallback` hook does for us! Every time we use `useCallback`, we create a closure, and the function that we pass to it is cached:

```tsx
// that inline function is cached exactly as in the section before
const onClick = useCallback(() => {}, []);
```

If we need access to state or props inside this function, we need to add them to the dependencies array:

```tsx
const Component = () => {
  const [state, setState] = useState();

  const onClick = useCallback(() => {
    // access to state inside
    console.log(state);

    // need to add this to the dependencies array
  }, [state]);
};
```

This dependencies array is what makes React refresh that cached closure, exactly as we did when we compared `value !== prevValue`. If I forget about that array, our closure becomes stale:

```tsx
const Component = () => {
  const [state, setState] = useState();

  const onClick = useCallback(() => {
    // state will always be the initial state value here
    // the closure is never refreshed
    console.log(state);

    // forgot about dependencies
  }, []);
};
```

And every time I trigger that callback, all that will be logged is `undefined`.

Play around with the code here:

{% embed https://codesandbox.io/embed/example3-3mfwv4?codemirror=1&hidedevtools=0&view=preview %}

<h2 id="part5">
  <a href="#part5">Stale closures in React: Refs</a>
</h2>

The second most common way to introduce the stale closure problem, after `useCallback` and `useMemo` hooks, is Refs.

What will happen if I try to use Ref for that `onClick` callback instead of `useCallback` hook? It's sometimes what the articles on the internet recommend doing to memoize props on components. On the surface, it does look simpler: just pass a function to `useRef` and access it through `ref.current`. No dependencies, no worries.

```tsx
const Component = () => {
  const ref = useRef(() => {
    // click handler
  });

  // ref.current stores the function and is stable between re-renders
  return <HeavyComponent onClick={ref.current} />;
};
```

However. Every function inside our component will form a closure, including the function that we pass to `useRef`. Our ref will be initialized only once when it's created and never updated by itself. It's basically the logic that we created at the beginning. Only instead of `value`, we pass the function that we want to preserve. Something like this:

```tsx
const ref = {};

const useRef = (callback) => {
  if (!ref.current) {
    ref.current = callback;
  }

  return ref.current;
};
```

So, in this case, the closure that was formed at the very beginning, when the component was just mounted, will be preserved and never refreshed. When we try to access the state or props inside that function stored in Ref, we'll only get their initial values:

```tsx
const Component = ({ someProp }) => {
  const [state, setState] = useState();

  const ref = useRef(() => {
    // both of them will be stale and will never change
    console.log(someProp);
    console.log(state);
  });
};
```

To fix this, we need to ensure that we update that ref value every time something that we try to access inside changes. Essentially, we need to implement what the dependencies array functionality does for the `useCallback` hook.

```tsx
const Component = ({ someProp }) => {
  // initialize ref - creates closure!
  const ref = useRef(() => {
    // both of them will be stale and will never change
    console.log(someProp);
    console.log(state);
  });

  useEffect(() => {
    // update the closure when state or props change
    ref.current = () => {
      console.log(someProp);
      console.log(state);
    };
  }, [state, someProp]);
};
```

Play around with the code here:

{% embed https://codesandbox.io/embed/example4-vmc3mr?codemirror=1&hidedevtools=0&view=preview %}

<h2 id="part6">
  <a href="#part6">Stale closures in React: React.memo</a>
</h2>

And finally, we're back to the beginning of the article and the mystery that initiated all this. Let's take a look at the problematic code again:

```tsx
const HeavyComponentMemo = React.memo(
  HeavyComponent,
  (before, after) => {
    return before.title === after.title;
  },
);

const Form = () => {
  const [value, setValue] = useState();

  const onClick = () => {
    // submit our form data here
    console.log(value);
  };

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <HeavyComponentMemo
        title="Welcome to the form"
        onClick={onClick}
      />
    </>
  );
};
```

Every time we click on the button, we log "undefined". Our `value` inside `onClick` is never updated. Can you tell why now?

It's a stale closure again, of course. When we create `onClick`, the closure is first formed with the default state value, i.e., "undefined". We pass that closure to our memoized component, along with the `title` prop. Inside the comparison function, we compare only the `title`. It never changes, it's just a string. The comparison function always returns `true`, `HeavyComponent` is never updated, and as a result, it holds the reference to the very first `onClick` closure, with the frozen "undefined" value.

Now that we know the problem, how do we fix it? Easier said than done here…

Ideally, we should compare every prop in the comparison function, so we need to include `onClick` there:

```tsx
(before, after) => {
  return (
    before.title === after.title &&
    before.onClick === after.onClick
  );
};
```

However, in this case, it would mean we're just reimplementing the React default behavior and doing exactly what `React.memo` without the comparison function does. So we can just ditch it and leave it only as `React.memo(HeavyComponent)`.

But doing that means that we need to wrap our `onClick` in `useCallback`. But it depends on the state, so it will change with every keystroke. We're back to square one: our heavy component will re-render on every state change, exactly what we tried to avoid.

We could play around with composition and try to extract and isolate either state or `HeavyComponent`. But it won't be easy: input and `HeavyComponent` both depend on that state.

We can try many other things. But we don't have to do any heavy refactorings to escape that closures trap. There is one cool trick that can help us here.

<h2 id="part7">
  <a href="#part7">Escaping the closure trap with Refs</a>
</h2>

This trick is absolutely mind-blowing: it's very simple, but it can forever change how you memoize functions in React. Or maybe not... In any case, it might be useful, so let's dive into it.

Let's get rid of the comparison function in our `React.memo` and `onClick` implementation for now. Just a pure component with state and memoized `HeavyComponent`:

```tsx
const HeavyComponentMemo = React.memo(HeavyComponent);

const Form = () => {
  const [value, setValue] = useState();

  return (
    <>
        <input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
        <HeavyComponentMemo title="Welcome to the form" onClick={...} />
    </>
  );
}
```

Now we need to add an `onClick` function that is stable between re-renders but also has access to the latest state without re-creating itself.

We're going to store it in Ref, so let's add it. Empty for now:

```tsx
const Form = () => {
  const [value, setValue] = useState();

  // adding an empty ref
  const ref = useRef();
};
```

In order for the function to have access to the latest state, it needs to be re-created with every re-render. There is no getting away from it, it's the nature of closures, nothing to do with React. We're supposed to modify Refs inside `useEffect`, not directly in render, so let's do that.

```tsx
const Form = () => {
  const [value, setValue] = useState();

  // adding an empty ref
  const ref = useRef();

  useEffect(() => {
    // our callback that we want to trigger
    // with state
    ref.current = () => {
      console.log(value);
    };

    // no dependencies array!
  });
};
```

`useEffect` without the dependency array will be triggered on every re-render. Which is exactly what we want. So now in our `ref.current` we have a closure that is recreated with every re-render, so the state that is logged there is always the latest.

But we can't just pass that `ref.current` to the memoized component. That value will differ with every re-render, so memoization just won't work.

```tsx
const Form = () => {
  const ref = useRef();

  useEffect(() => {
    ref.current = () => {
      console.log(value);
    };
  });

  return (
    <>
      {/* Can't do that, will break memoization */}
      <HeavyComponentMemo onClick={ref.current} />
    </>
  );
};
```

So instead, let's create a small empty function wrapped in `useCallback` with no dependencies for that.

```tsx
const Form = () => {
  const ref = useRef();

  useEffect(() => {
    ref.current = () => {
      console.log(value);
    };
  });

  const onClick = useCallback(() => {
    // empty dependency! will never change
  }, []);

  return (
    <>
      {/* Now memoization will work, onClick never changes */}
      <HeavyComponentMemo onClick={onClick} />
    </>
  );
};
```

Now, memoization works perfectly - the `onClick` never changes. One problem, though: it does nothing.

And here's the magic trick: all we need to make it work is to call `ref.current` inside that memoized callback:

```tsx
useEffect(() => {
  ref.current = () => {
    console.log(value);
  };
});

const onClick = useCallback(() => {
  // call the ref here
  ref.current();

  // still empty dependencies array!
}, []);
```

Notice how `ref` is not in the dependencies of the `useCallback`? It doesn't need to be. `ref` by itself never changes. It's just a reference to a mutable object that the `useRef` hook returns.

But when a closure freezes everything around it, it doesn't make objects immutable or frozen. Objects are stored in a different part of the memory, and multiple variables can contain references to exactly the same object.

```tsx
const a = { value: 'one' };
// b is a different variable that references the same object
const b = a;
```

If I mutate the object through one of the references and then access it through another, the changes will be there:

```tsx
a.value = 'two';

console.log(b.value); // will be "two"
```

In our case, even that doesn't happen: we have exactly the same reference inside `useCallback` and inside `useEffect`. So when we mutate the `current` property of the `ref` object inside `useEffect`, we can access that exact property inside our `useCallback`. This property happens to be a closure that captured the latest state data.

The full code will look like this:

```tsx
const Form = () => {
  const [value, setValue] = useState();
  const ref = useRef();

  useEffect(() => {
    ref.current = () => {
      // will be latest
      console.log(value);
    };
  });

  const onClick = useCallback(() => {
    // will be latest
    ref.current?.();
  }, []);

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <HeavyComponentMemo
        title="Welcome closures"
        onClick={onClick}
      />
    </>
  );
};
```

Now, we have the best of both worlds: the heavy component is properly memoized and doesn't re-render with every state change. And the `onClick` callback on it has access to the latest data in the component without ruining memoization. We can safely send everything we need to the backend now!

Play around with the fixed example here:

{% embed https://codesandbox.io/embed/example5-h3jl52?file=/App.tsx?codemirror=1&hidedevtools=0&view=preview %}


---

Hopefully, all of this made sense, and closures are now easy-peasy for you. A few things to remember about closures, before you go:

- Closures are formed every time a function is created inside another function.
- Since React components are just functions, every function created inside forms a closure, including such hooks as `useCallback` and `useRef`.
- When a function that forms a closure is called, all the data around it is "frozen", like a snapshot.
- To update that data, we need to re-create the "closed" function. This is what dependencies of hooks like `useCallback` allow us to do.
- If we miss a dependency, or don't refresh the closed function assigned to `ref.current`, the closure becomes "stale".
- We can escape the "stale closure" trap in React by taking advantage of the fact that Ref is a mutable object. We can mutate `ref.current` outside of the stale closure, and then access it inside. Will be the latest data.

The video based on the material in this article is available below. It's less detailed but has nice animations and visuals, so it could be useful to solidify the knowledge.

{% embed https://www.youtube.com/embed/AhAGA5LUxek %}

---

This article is part of the ["Advanced React"](https://www.advanced-react.com) book. If you like it, you might like the book as well 😉

---

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this.

[Subscribe to the newsletter](https://www.developerway.com), [connect on LinkedIn](https://www.linkedin.com/in/adevnadia/) or [follow on Twitter](https://twitter.com/adevnadia) to get notified as soon as the next article comes out.