React: Buggy with closure
React: Buggy with closure
Closure is one of the most basic concepts which every web developer should know about. But understanding it is not a piece of cake. Even though we might know what it actually does, the real use cases and how it could affect our code is another story. Having been working with React for a while, sometimes I encountered some bugs which sometimes make me confused. One of them is about the bug when working with closure in React.
Let’s start with the definition of closure (for people who don’t know about it). The closure is a function that is within another function and can access and use the variable of the outer function. Even if the outer function is returned.
Let’s take a look at an example:
const outer = () => {
const outerVariable = 'a';
const inner = () => {
const innerVariable = 'b';
return `${outerVariable}-${innerVariable}`;
}
}
console.log(outer()); // a-bconst outer = () => {
const outerVariable = 'a';
const inner = () => {
const innerVariable = 'b';
return `${outerVariable}-${innerVariable}`;
}
}
console.log(outer()); // a-bBut what does it mean by “even if the outer function is returned?”
Let’s consider the following example:
const outer = () => {
let outerVariable = 10;
const inner = () => {
const innerVariable = 11;
console.log(
"outerVariable",
outerVariable,
"innerVariable",
innerVariable
);
outerVariable += 1;
return innerVariable + outerVariable;
};
return inner;
};
const x = outer();
const y = outer();
console.log(x());
console.log(x());
console.log(x());
console.log(y());
// output
const outer = () => {
let outerVariable = 10;
const inner = () => {
const innerVariable = 11;
console.log(
"outerVariable",
outerVariable,
"innerVariable",
innerVariable
);
outerVariable += 1;
return innerVariable + outerVariable;
};
return inner;
};
const x = outer();
console.log(x());
console.log(x());
console.log(x());
// output
outerVariable 10 innerVariable 11
22
outerVariable 11 innerVariable 11
23
outerVariable 12 innerVariable 11
24const outer = () => {
let outerVariable = 10;
const inner = () => {
const innerVariable = 11;
console.log(
"outerVariable",
outerVariable,
"innerVariable",
innerVariable
);
outerVariable += 1;
return innerVariable + outerVariable;
};
return inner;
};
const x = outer();
const y = outer();
console.log(x());
console.log(x());
console.log(x());
console.log(y());
// output
const outer = () => {
let outerVariable = 10;
const inner = () => {
const innerVariable = 11;
console.log(
"outerVariable",
outerVariable,
"innerVariable",
innerVariable
);
outerVariable += 1;
return innerVariable + outerVariable;
};
return inner;
};
const x = outer();
console.log(x());
console.log(x());
console.log(x());
// output
outerVariable 10 innerVariable 11
22
outerVariable 11 innerVariable 11
23
outerVariable 12 innerVariable 11
24Now when we assign outer() to x and y, the outer function actually finished its work, it returned the inner function. Now each time we call x or y, we’re calling the inner function, not the outer function one.
But as you can see, we still can access as well as manipulate the outer function variable. :D Even though the outer function was returned.
Okay, we now have a basic idea of how closure work, so, how closure is applied to React? I think that everyone who’s working with React is dealing with Closure every day and in every single component, you wrote.
let’s take a look at the following simple example:
import { useState } from "react";
export default function App() {
const [test, setTest] = useState("a");
const logTest = () => {
console.log("test", test);
};
return (
<div className="App">
<p>{test}</p>
<button onClick={logTest}>Change test</button>
</div>
);
}import { useState } from "react";
export default function App() {
const [test, setTest] = useState("a");
const logTest = () => {
console.log("test", test);
};
return (
<div className="App">
<p>{test}</p>
<button onClick={logTest}>Change test</button>
</div>
);
}When you writes React component, keep it in mind that you just create some function and React do inner stuff to transform it into UI. So, in the logTest, you’re accessing the test variable which is not declared in its scope but in outer function (the App function). See? It’s closure.
what's the problem you should notice when working with React as well as Closure
Let me give you another example:
import { useState } from "react";
export default function App() {
const [test, setTest] = useState("a");
const changeTest = () => {
setTest("b");
anotherFuncion();
};
const anotherFuncion = () => {
console.log("test", test);
};
return (
<div className="App">
<p>{test}</p>
<button onClick={changeTest}>Change test</button>
</div>
);
}import { useState } from "react";
export default function App() {
const [test, setTest] = useState("a");
const changeTest = () => {
setTest("b");
anotherFuncion();
};
const anotherFuncion = () => {
console.log("test", test);
};
return (
<div className="App">
<p>{test}</p>
<button onClick={changeTest}>Change test</button>
</div>
);
}When you click the button Change test, you would expect to see that latest value of test in anotherFunction.
But when it's not
But it doesn't how React works. When you change the state of React component, the component will be re-rendered and in the next render, you would see that value of the state you've just updated, not in the current render. So if you try accessing the state in anotherFunction, the test state will remain the value of the current render which is “a" in this case.
An another example for closure in React would be
import { useEffect, useState } from "react";
export default function App() {
const [counter, setCounter] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
console.log('counter', counter);
setCounter(counter + 1);
}, 1000);
return () => {
clearInterval(interval);
}
}, [])
return (
<div className="App">
<p>{counter}</p>
</div>
);
}import { useEffect, useState } from "react";
export default function App() {
const [counter, setCounter] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
console.log('counter', counter);
setCounter(counter + 1);
}, 1000);
return () => {
clearInterval(interval);
}
}, [])
return (
<div className="App">
<p>{counter}</p>
</div>
);
}We would expect to see the counter is increased by 1 every second. But it’s not.
The reason for this is still closure
the useEffect with empty deps array will only be called once after the first render of the component. So the setInterval is initialized once. and at that time, it only sees the counter has value 1 of the App component and uses that value all the time it calls the callback function. No matter how many times our component is re-rendered, the setInterval won’t know about the changes and still use the initial value. You have to understand that the setInterval won’t know about the change because the counter the setInterval uses the initial counter (or the ref to the initial counter) and each time the component is re-rendered, a new counter is created (which has a different ref from the previous one).
So, the solution to both above cases is: we need to somehow specify the variable (the ref) of the inner function to be the variable (the ref) of the latest state.
To archive this, there are 2 common ways. Note that I would just create examples of those solutions for the interval issue, the first issue would be solved in the same way.
We know that when we init a variable using useRef (if you’re using hooks) or createRef(if you’re still a fan of class component), that variable will not change even if our component is re-rendered. In the other way, the reference of that variable is immutable. Let’s see how we solve above issue by using ref
import { useEffect, useRef, useState } from "react";
export default function App() {
const counterRef = useRef(1);
const [counter, setCounter] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
console.log('counter', counterRef.current);
setCounter(counter + 1);
counterRef.current = counterRef.current + 1;
}, 1000);
return () => {
clearInterval(interval);
}
}, [])
return (
<div className="App">
<p>{counter}</p>
<p>counter ref {counterRef.current}</p>
</div>
);
}import { useEffect, useRef, useState } from "react";
export default function App() {
const counterRef = useRef(1);
const [counter, setCounter] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
console.log('counter', counterRef.current);
setCounter(counter + 1);
counterRef.current = counterRef.current + 1;
}, 1000);
return () => {
clearInterval(interval);
}
}, [])
return (
<div className="App">
<p>{counter}</p>
<p>counter ref {counterRef.current}</p>
</div>
);
}Look at the console, wohoo, we’ll see the latest value of the counter. Wohoo. But, wait a minute, why my component still shows both counter and counterRef.current are 1?. the counter is 1 is reasonable (because of closure), the counter ref shown in the screen is also 1 because changing the ref doesn’t trigger the re-render, so even though the value of ref.current changed, the component isn’t re-rendered which leads to the result that it still shows 1 for counter ref.
You might wonder what’s the point of this solution, if my component isn’t re-render, this would be useless, right? It’s because my example doesn’t really show the benefit of this solution. In the real-life, you might want to do some other side effects like refetching the data when the counter changes (bear with me, it’s just an example) or calculate a new state based on the counter,... most of the time all of those operations will update some states of your component which component re-rendering.
If the first solution is just “using the variable with fixed reference’, the second solution is “create new closure function to catch up with the latest reference of the state”
Let’s see what we would do
import { useEffect, useState } from "react";
export default function App() {
const [counter, setCounter] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearInterval(interval);
}
}, [counter])
return (
<div className="App">
<p>{counter}</p>
</div>
);
}import { useEffect, useState } from "react";
export default function App() {
const [counter, setCounter] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearInterval(interval);
}
}, [counter])
return (
<div className="App">
<p>{counter}</p>
</div>
);
}We know that useEffect with a list of deps will run the callback function each time one of the variable in its deps list changed. So we just basically “listen” to changes of counter (after it’s set by the setCounter in our setInterval callback), and each time the counter is updated, we will create a new interval with the latest reference of the counter state. it would solve our issue.
One downside of this approach is that we know that all useEffect will run their callback functions on the first time the component is mounted no matter its deps list is empty / has some variables or none. It might not be a big deal because sometimes it’s our desired behavior, but I just want to point it out.
Okay, I’ve just shared with you guys one of the issue when working with React which I think sometimes confuses newbies (☹️ I have to admit that up until now sometimes I still encounter this bug and have to debug for a while until I realized how stupid I’m). Hope it would help.
Bye-bye, see you guys next time.