Some Updatesπ
I am happy to say that I am DONE with the SEMESTERRRR RAHHHH π₯³π₯³π₯³. I had a lot of fun classes this semester! Got to work on a bunch of cool small projects with Java, JavaScript, and Python. Now that I am done, I can get back to what's truly important in life. Writing blogs, and playing Fall guys.
Something I'm super excited about is that I'm joining Grafana as an intern on June 5th πππ₯³! Until then I'll be giving it my all with the CRM project at Emerging Tech, and getting ready by brushing up on Golang and Docker.
The Post
In this post I wanted to talk briefly about closures in JavaScript and React. Closures are a feature in many lexically scoped programming languages that is especially useful for supporting functions as first-class objects.
First-class objects are pretty much entities in programming that you can do ANYTHING with. Passing it into a function? You got it. Creating or Destroying one? By all means. Store it in a data structure? Go crazy bro. Essentially, first-class objects support all the operations available to any other type of object.
Closures are a byproduct of giving functions all these perks and privileges. Say that you are building the next great Minecraft 1.8 PvP training app. Users have to click a button as fast as possible, getting as many clicks in as possible. If they take more than 1 second between clicks, the amount of times the user clicked is printed, and the count resets to 0.
In order to do this, you make a 'useCounter' hook that has a debouncer that times out after a second, resetting the click count.
const useCounter = () => {
const [clicked, setClicked] = useState(0);
let timeoutFunc;
let handleClick = () => {
if (timeoutFunc) clearTimeout(timeoutFunc);
setClicked(clicked + 1);
timeoutFunc = setTimeout(() => {
console.log(`You clicked ${clicked} times`);
setClicked(0);
}, 1000); // 1000 seconds
};
return [handleClick];
};
When you call this hook, you return handleClick and you can slap that onto any button event listener. However, how does handleClick know the value of the variables that existed around (timeoutFunc, clicked, setClicked) it if the scope in which they resided is already gone?
This is where closures come in clutch. When the function is created, a closure is generated which bundles any necessary objects/entities/variables in the surrounding scope with the function as its returned.
However these closures can sometimes capture seemingly outdated variables. In this example, our setTimeout callback generates a closure with outdated state variables. What gets printed out after the debounce function completes is actually 1 less than the total amount we clicked!
What really exacerbates this problem is that react values get set asynchronously. The value of 'clicked' does not update until the next re-render, and when the setTimeout
callback captures the variable it gets the old clicked
value rather than the one we would expect.
Fixing Closures
The example shown above is trivial and can be fixed pretty easily. There are also a couple of common stale closure problems one could encounter when using React hooks.
- useEffect Stale Closures: These can happen in your useEffect when you call functions that involve callbacks, and these callbacks themselves use some aspect of state. Stuff like
setTimeout
andsetInterval
. Most of the time these can be fixed by setting up your useEffect dependencies and cleanup functions correctly. - useState Stale Closures: These can happen when you try to update your state multiple times in rapid succession by using your state variable as reference. Stuff like
setCount(count + 1)
, which is susceptible to using an outdatedcount
value. This is the reason why its recommended to instead use callbacks (setCount(count => count + 1)
), since these eensure the correct "previous" value of the state is being used
Truly Top Ten Worst Practices
A while back during Spring Break, I was working on a class project called SweetBeats which was meant to serve as a music production webapp. Our group chose to use ToneJS as a way to interface with the WebAudio API. ToneJS has a Sequencer Object that takes a callback that can be used to conditionally play notes at certain points of the track.
The deal with this Sequencer Object is that we couldn't really clean it up and remount it on every state change without there being some cutoff in the audio. What's even crazier, one of the state values used inside of this callback directly affected the UI. I'm sure this was something that could have been fixed with some combination of useStates and useRefs.
However our group was on a time crunch. We needed to COOK IMMEDIATELY. I needed a reference, and I knew that JavaScript Objects get passed around as references (or rather, passed as value but the value itself is a reference π€―), so I tried that out first. I'll use the debounce example again.
// What if we tried an object???
let [clicked, setClicked] = useState({clicked: 0});
let timeoutFunc;
let handleClick = () => {
setClicked((prev) => {
prev.count = prev.count + 1;
return prev;
});
if (timeoutFunc) clearTimeout(timeoutFunc);
timeoutFunc = setTimeout(() => {
console.log(`You clicked ${clicked.count} times`);
setClicked((prev) => {
prev.count = 0;
return prev;
});
}, 1000); // 1000 seconds
};
I do not like this at all. However, it did succeed in making it so the callback function always had the latest value of state. I had something similar to this in the actual code with ToneJS, but I soon realized it wouldn't work as I needed both the latest value PLUS it needed to trigger re-renders. When React evaluates our setClicked, it can actually tell that the reference to the object itself has not changed, and assumes that there was no change in state.
It seemed like all hope was lost until I came up with the craziest 5head worst practice of all time. Nest ANOTHER object inside of our object, destructure the outer object, but keep the reference to the inner object. WHAT THE HEEEEEEEEEEEEEEEEEEELLLLL.
let [clicked, setClicked] = useState({ count: { innerObj: 0 } });
let timeoutFunc;
let handleClick = () => {
setClicked((prev) => {
let newObj = { ...prev };
newObj.count.innerObj = newObj.count.innerObj + 1;
return newObj;
});
if (timeoutFunc) clearTimeout(timeoutFunc);
timeoutFunc = setTimeout(() => {
console.log(`You clicked ${clicked.count.innerObj} times`);
setClicked((prev) => {
let newObj = { ...prev };
newObj.count.innerObj = 0;
return newObj;
});
}, 1000); // 1000 seconds
};

The Magnum Opus of all of Dan Abramov's work, ever. useRef that useStates. We pass our state around by reference, it is maintained accross re-renders, AND we can trigger re-renders while preserving the reference of our data. This actually fixed our problem with ToneJS, but it is certainly not a sustainable fix. I'm not sure what goes on under the hood when React handles updating the values but I'm sure its not good for performance reasons, financial reasons, ethical reasons etc. Thought it would be something cool to showcase on the blog if anybody else is just finding this out π€―.
That's it! Thanks for reading my post. Stay tuned for more π.
What I'm BUMPIN Today
Its been a while so here's a new list π