Next up in this series on practical React strategies we are going to focus on one of the unsung heroes of the JS world, closures. Before we dive into the practical applications of closures in React lets define what a closure is!
Breaking Down Closures
The most straightforward definition of a closure is a function that returns a function. For example, you might want a function to multiply two values, one of which is static and the other could be dynamic. Let's take a look at what that means:
function multiplyValues(staticNum) {
return function(dynamicNum) {
return staticNum * dynamicNum
}
}
// times2 is now the function that was returned in multiplyValues
const times2 = multiplyValues(2)
console.log(times2(5)) // prints 10
console.log(times2(100)) // prints 200
So our closure function, multiplyValues
, takes an argument for a static number that will always be used as the multiplier for the dynamic number passed to the inner function. The applications of this technique are numerous, but admittedly are less straightforward when we're thinking through our logic trees. The same result could be done without a closure like this:
const multiplyValues = (numA, numB) => {
return numA * numB
}
console.log(multiplyValues(2, 5)) // prints 10
console.log(multiplyValues(2, 100)) // prints 200
The main difference is that we are repeating the pseudo-constant value of 2 to the generic multiplyValues
function. If we know that we want to multiply by 2 in various sections of our code, it is easier understand that relationship by assigning the "static" value to a function variable and using that variable throughout the codebase.
Closures in React
Now that we have a solid understanding of what closures are in general let's talk about the ways we can use them in React.
Setting State
Local state management is integral to most React components. One way we can use state is to determine what kind of content is rendered. For example, let's say we have a component that renders 3 tabs, each with its own content. We've decided to set which tab is open in state and will render the appropriate content based on which tab is currently selected. The following is a non-closure version of handling the state for this component.
import React, { useState } from 'react'
const TabRenderer = () => {
const [selectedTab, setSelectedTab] = useState('first')
const handleFirst = () => setSelectedTab('first')
const handleSecond = () => setSelectedTab('second')
const handleThird = () => setSelectedTab('third')
const renderTabContent = () => {
switch(selectedTab) {
case 'first':
return <div>First</div>
case 'second':
return <div>Second</div>
case 'third':
return <div>Third</div>
default:
return <div>First</div>
}
}
return (
<div>
<div>
<button onClick={handleFirst}>First</button>
<button onClick={handleSecond}>Second</button>
<button onClick={handleThird}>Third</button>
</div>
<div>{renderTabContent()}</div>
</div>
)
}
This works and is fairly straightforward to read. But we're allocating 3 separate functions to set the same state. We could forego declaring these functions earlier in the code and just do it in each button like onClick={() => setSelectedTab('first')}
. I'd argue you actually lose a bit of readability by declaring functions in the JSX and I generally advocate against that practice. Let's take a look at how could we refactor this with a closure.
const TabRenderer = () => {
const handleClick = (newTab) => () => {
setSelectedTab(newTab)
}
return (
...
<div>
<button onClick={handleClick('first')}>First</button>
<button onClick={handleClick('second')}>Second</button>
<button onClick={handleClick('third')}>Third</button>
</div>
...
)
}
By defining one function we've reduced the amount of memory being allocated by our application and set ourselves up to more easily change the internals of our click handler. This is by far the most common case for closures in React, although it is certainly not the only one.
Event Handling
Another complex piece of code in React is often form event handling. Generally we need to update the changed state of the object we are interacting with along with doing some sort of error validation or additional data parsing. Most of the time you can deal with this by accurately setting your name/value attributes on the input or select elements. But what happens when you need to map a user selected value to a dynamic value via a select
element?
const OPTIONS = ['url', 'name']
const ComplexForm = (props) => {
const { onChange, mappedData, dataStringArray } = props
const handleChange = (userColumn) => (event) => {
const mappedValue = event.target.value
onChange({ ...originalData, [userColumn]: mappedValue })
}
const renderColumn = (userColumn) => {
return (
<div>
<p>{columnName}</p>
<select onChange={handleChange(userColumn)}>
{OPTIONS.map((opt) => (
<option value={opt}>{opt}</option>
))}
</select>
</div>
)
}
return (
<div>
{dataStringArray.map(renderColumn)}
</div>
)
}
The user defined strings are completely dynamic but the goal is to relate them to a string that is expected by our backend. By using a closure we're able to dynamically associate the column/dropdown option pairs that will get mapped by the user.
Wrapping up
With our friend the closure we are able to more efficiently use memory in our applications and consolidate the surface area of potential changes. If you've used closures before, comment with some of your prior experiences. I'd love to keep the conversation going by sharing examples!