Closures are a powerful tool in JavaScript. If you've been writing event listeners, your own custom hooks, or even using the default hooks React is built with, then you've been using closures already. But what specifically are closures? I'll attempt to explain what they are in this post.
Scope
In order to understand closures, we need to understand scope. To summarize, when something is locally scoped, it can only be "seen" by other things within its scope. If I declare a local variable inside of a code block, then it can't be seen by anything outside of that block.
function localBlock() {
const x = 4
console.log(x) //this prints 4
}
console.log(x) //x is undefined
In human terms, this is kind of like entering a soundproof room with your friend and telling them a secret. No one outside of the room will hear or know it.
Closures
With scopes out of the way, we can focus on the reason I'm making this post. Closures are a way for JavaScript to "remember" or "preserve" variables that are no longer in scope. Let's consider the following code.
function outerScope(){
const x = 100
const innerScope = function(){
console.log(x)
}
return innerScope
}
console.log(x) //prints undefined
const closureExample = outerScope()
closureExample() //prints 100
The function outerScope
defines two variables. It assigns x
the value of 100, and innerScope
the value of a function. It then returns the function, which still has access to x
.
The function innerScope
technically has access to everything that was defined inside of outerScope
In human terms, I'll reuse the secret room analogy from before. You and your friend enter a soundproof room and you tell them a secret. Nobody outside of the room knows what the secret is. But your friend will still know the secret even after they've left the room and they're free to tell whoever they want.
What are closures used for?
In the beginning of this post, I named some ways in which you may have been using closures already. Closures are also used for currying / nesting functions, factory functions, and partial applications.
As a (somewhat contrived) example, let's say you're writing a small game in React. You have a button a user can click to increment a counter. After 5 seconds have passed, an alert box pops up to show the user how many times they clicked the button. The code would look something like this.
function App(){
const [counter, setCounter] = useState(0)
const increment = () => setCounter(c => c + 1)
useEffect(() => {
setTimeout(() => alert(`You clicked the button ${counter} times!`), 5000)
}, [])
return (<div>
<button onClick={increment}>Counter: {counter}</button>
</div>)
}
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
In this example, the anonymous function we passed to setTimeout contains a closure around the initial value for counter. No matter how many times we click the button, the alert box will tell us You clicked the button 0 times!
So when our app component mounts, the useEffect function runs, and our anonymous function that we passed to setTimeout "remembers" the value for counter.
Extra: Why this happens and fixing our component
If you're wondering why useEffect
doesn't work the way you want it to, it's because of how React hooks handle state. State is never mutated. React really doesn't want us to do this. When we update state with our setCounter
function, we're creating an entirely new value altogether. This new value isn't tracked by the function in our useEffect at all.
To fix this, we would some way to track our value without mutating our state. Fortunately, the useRef hook allows us to do just that. To keep things brief, the useRef hook creates an object with a current
property. We'll assign the current property to our counter variable. The object created by useRef CAN be mutated, so we never have to worry about losing the value we assign to it. Every time we click the button, our component re-renders and our countRef object's current property is assigned the new counter value.
function App(){
const countRef = React.useRef()
const [counter, setCounter] = React.useState(0)
const increment = () => setCounter(c => c + 1)
countRef.current = counter
React.useEffect(() => {
setTimeout(() => alert(`You clicked the button ${countRef.current} times!`), 5000)
}, [])
return (<div>
<button onClick={increment}>Counter: {counter}</button>
</div>)
}
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
Top comments (0)