Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I avoid this stale closure?

I am working on building a simple React slider which will expose internal methods up to its parent via a ref and I am having trouble with what I suspect to be a stale closure, but I can't fully understand what is actually happening. Hoping someone can help me understand here.

Here is a simplified version of the code that I want to work:

const Slider = forwardRef((props, ref) => {
  const sliderRef = useRef();
  const [slides, dispatchSlides] = useReducer(reducer, []);

  sliderRef.current = {
    countSlides: () => {
      return slides.length
    },
  };

  useImperativeHandle(ref, () => sliderRef.current);

  return null;

After this component mounts, its children will render and fill up the slides reducer with information on their positioning and visibility using IntersectionObserver. This part works, so I have kept it out of this example for simplicity. For our sake, just assume that slides is immediately populated with objects after mount, and that a user will manually call countObjects from the parent component much later after slides has been populated.

In the parent component, if I execute countSlides from the ref, I will always see slides.length === 0, no matter how many slides are actually present. I assume this is because the original countSlides method is a stale closure.

Now, what I don't fully understand, is that if I adjust this line:

  useImperativeHandle(ref, () => sliderRef.current);

to this:

  useImperativeHandle(ref, () => () => {
    countSlides: () => sliderRef.current.countSlides()
  });

the stale closure is fixed and everything works as intended. But this is duplicative code and I'm just not sure what is even different between the two cases. I do not want to repeat myself redefining many methods within the useImperativeHandle hook, but much more importantly, I want to understand what the difference is between the two examples above.

Thank you!

EDIT Adding full example:

https://codesandbox.io/s/ssr-slider-6ywf9

like image 503
jmikrut Avatar asked Jun 17 '26 18:06

jmikrut


1 Answers

As you commented that the problem arose only when writing like onClick={ slider?.current?.prev } instead of onClick={() => { slider?.current?.prev() }}

I have tried with my sandbox that I provided and got the same problem.

There're a few things here:

  1. useRef doesn't trigger re-renders itself, which means even a ref is updated, no re-renders follow.
  2. Without re-renders, what's bound to onClick will not be updated.

So, if we write like onClick={slider?.current?.prev}, what happens is:

  1. The ref is initially undefined, which means onClick is undefined as well
  2. No re-render is triggered, so, even if ref is updated with a new value, onClick stays undefined

But, if we write like onClick={() => { slider?.current?.prev() }}, what happens is:

  1. slider?.current?.prev is initially undefined
  2. onClick is bound to that anonymous function
  3. slider?.current?.prev is updated, we have the expected function
  4. When the button is clicked, the function is called, which triggers the latest value of slider?.current?.prev
like image 127
hungdoansy Avatar answered Jun 20 '26 08:06

hungdoansy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!