Okay, the title is a bit of click-bait. There are times when you need to use anonymous functions as props, but they're probably a lot less thank you think. But first, let's describe the issue.
Anonymous Functions as Props
In component libraries like Svelte and React, anonymous functions used as component props have become a kind of lazy go-to that threatens bloat at larger scale.
I see so many devs do this:
<button onclick={() => handleSubmit()}>Submit</button>
Instead of this:
<button onclick={handleSubmit}>Submit</button>
Each of these code blocks will literally accomplish the exact same result. The only difference is that an anonymous function has been interposed in the first example, and all it does is call a named function with no arguments.
Do you see why using the anon approach could be a problem? Every time this button renders, a brand new anon function is created and held in memory. If you have ten of these rendered on the same page, you have ten distinct anonymous functions held in memory.
Now you might say, "Okay, but how often am I going to have more than, say, ten or twenty interactive elmements on a page?" And the answer is, "You might think it's an edge case, but it comes up more than you might guess. And you might as well be ready for the edge with good habits!"
A prime example of where this can be a problem is in larger tables or lists with interactive features, such as deleting or editing a row or item. If you've got, for example, a data grid type of component on your page and your pagination for it is, say, 100 rows per page, then whatever anonymous function every row creates will be there 100 times. If you've got a checkbox that does something on every row, along with Edit and Delete buttons, you now have 300 anonymous functions held in memory, along with the original 3 functions named and declared, getting called by the anonymous functions.
It's even worse if do trickery like pre-rendering but hiding the next page of data for efficiency's sake. In that example, you now have 600 anonymous function instances sitting in memory.
Okay, then Why Do People Do This?
I can think of at least two reasons why this habit is so prevalent and ingrained.
Reactivity or Other Desired Behavior
At least in Svelte, there are times when you need to do this to ensure the function is called reactively. There may be other types of situations that require you to do this to get your logic to work as predicted, and although it's been a few years since I've worked with React, I bet there are some similar examples in that library too.
But these are edge cases you can address as needed while coding.
Simplifying the passing of function params
This one is likely the most common culprit. It allows you to pass some kind of state directly into the function, so it's easier to grasp and work with when you first learn a UI library like Svelte or React. That's maybe why most tutorials show anonymous functions as props.
Here's an example (in Svelte 5):
// EditButton.svelte
<script>
// productId is sent to this component as a prop
let { productId } = $props()
</script>
const onEditToggle = (productId) => {
// Do some stuff with productId...
}
<button onclick={() => onEditToggle(productId)}>
Edit
</button>
Nice and clean, right? But we have this potential performance issue if we have hundreds of instances of this button on this page. How do we refactor this to be more potentially performant?
The Simpler Solution
As is often the case in programming, a simple and straightforward solution is to make sure we create individual components for things like EditButton, and then we have the productId scoped just to that instance. So, we don't have to pass anything to our handler function because it's inside a discrete component that already knows what that productId is. This is why it's generally good if code is more modular than monolithic.
You should definitely attempt to go for this solution first, if you can. And notice that because our component is isolated to handling only one productId, we just don't need that anonymous function. Instead, our component function can handle it directly.
// EditButton.svelte
<script>
let { productId } = $props()
</script>
// This is all you really have to do in a component isolated like this!
const onEditToggle = () => {
// Do some stuff with productId, because we already have it
// as a prop in memory and scoped here.
}
<button onclick={onEditToggle}>
Edit
</button>
The More Complex Solution
But there are times when we can't have the update logic confined to a local scale like this due to system architecture or whatever else. To handle this kind of situation, we do something that is, yes, a little bit more complex and, I hate to say it, is slightly less declarative than sending an anon function as a prop. But it's still very readable and works for our purposes of not pushing a bunch of anon functions into memory.
It involves using data attributes on the html element that gets the event you're handling (such as click). And it looks like this:
// EditButton.svelte
<script>
// productId and onEditToggle are sent to this component as props
let { productId, onEditToggle } = $props()
</script>
<button onclick={onEditToggle} data-product-id={productId}>
Edit
</button>
// DataGridRow.svelte
<script>
import EditButton from "./EditButton.svelte"
let { productId, onEditToggle } = $props()
</script>
<tr>
// ... other cells in the row
<td><EditButton {productId} {onEditToggle} /></td>
</tr>
// DataGrid.svelte
<script>
import DataGridRow from "./DataGridRow.svelte"
let { products } = $props()
const onEditToggle = event => {
// Retrieve product ID from element data attribute
const productId = event.currentTarget.getAttribute("data-product-id")
// Then do some stuff with that ID...
}
</script>
{#each products as product}
<DataGridRow productId={product.id} {onEditToggle} />
{/each}
Do you see what's happening there? We put the product ID into a custom data attribute on the html button element inside EditButton. When the handler function fires, it can retrieve the product ID right off the element's data attribute.
Now we have just one function, onEditToggle, declared in memory, and everything else just points to it by reference.
The Balance Between Readability and Performance
My personal feeling is to always begin with code so well-modularized that the passing of key data is done through props right to that component, rather than having the component be monolithic and have to determine all this internally. This is what I'm describing above in the "The Simpler Solution."
If you absolutely can't do that, then go for the second solution with data attributes.
Now, you could argue that because using anon functions is a little bit more readable than handling data attributes, it's the better thing to start with, so you could just adapt it later if you encounter performance issues.
I agree with that line of thinking generally, but this is one case in which complications from doing it this way are a little bit common enough to just always do it the same way. This way you don't have to think about whether/when to use one approach versus the other. They both work, and one won't require refactoring later if you find you have these performance problems.
Finally, a Caveat
It's entirely possible that Svelte somehow handles this during transpilation to smooth over these anon functions somehow. I'm not enough of an expert on Svelte's runtime or compiler to say for sure. But I personally feel that this is a safer pattern that applies to any JS library you might wind up using, and so it's just a better habit to adopt up-front.
What do you think? Do you have counterpoints? Or maybe can provide insight into what happens at a runtime and compilation level with Svelte that might change my opinion? Let me know in the comments!
Top comments (0)