Functions, this and Closures · Lesson 4 of 25
Functions and Arrow Functions in LWC
In a component you write functions in three places: methods on the class (event handlers, helpers), callbacks you pass to array methods, promises and timers, and plain functions in a module that several components share. They're all the same language feature, but each place has its own habits.
In this lesson
- Declarations, expressions and arrow functions
- Default and rest parameters
- Callbacks and higher-order functions
- Event handler methods and reading data-* attributes
Three ways to write a function
// 1. declaration – hoisted, so you can call it before the line it's on
function formatName(first, last) {
return `${last}, ${first}`;
}
// 2. expression – a function stored in a variable
const isClosed = function (stage) {
return stage.startsWith('Closed');
};
// 3. arrow function – short syntax, no own `this`
const toUpper = (text) => text.toUpperCase(); // one expression: implicit return
const toOption = (value) => ({ label: value, value }); // returning an object: wrap it in ( )
console.log(formatName('Asha', 'Verma'));
console.log(isClosed('Closed Won'), isClosed('Proposal'));
console.log(toUpper('lwc'), toOption('Hot'));
Verma, Asha
true false
LWC { label: 'Hot', value: 'Hot' }
The rule of thumb in LWC code: use methods for anything on the component, arrow functions for callbacks, and declarations for helpers in shared modules.
Default and rest parameters
function buildQueryLabel(objectName, limit = 10, ...fields) {
const list = fields.length ? fields.join(', ') : 'Id';
return `SELECT ${list} FROM ${objectName} LIMIT ${limit}`;
}
console.log(buildQueryLabel('Account'));
console.log(buildQueryLabel('Contact', 5, 'Name', 'Email'));
console.log(buildQueryLabel('Lead', undefined, 'Company')); // undefined → default used
// options object: named arguments, any order, easy to extend
function showMessage({ title, variant = 'info', mode = 'dismissible' } = {}) {
return `[${variant}/${mode}] ${title}`;
}
console.log(showMessage({ title: 'Saved', variant: 'success' }));
console.log(showMessage()); // no argument: the = {} default stops a crash
SELECT Id FROM Account LIMIT 10 SELECT Name, Email FROM Contact LIMIT 5 SELECT Company FROM Lead LIMIT 10 [success/dismissible] Saved [info/dismissible] undefined
The options-object pattern (a destructured parameter with defaults) is everywhere on the platform – ShowToastEvent, LightningModal.open(), createRecord() all take one object. Once a function needs more than two or three arguments, write yours the same way.
Callbacks and higher-order functions
A function that takes or returns another function is a higher-order function. You use them constantly – map, filter, then, setTimeout – and you can write your own:
const opportunities = [
{ Name: 'Acme', Amount: 50000, StageName: 'Proposal' },
{ Name: 'Globex', Amount: 120000, StageName: 'Closed Won' },
{ Name: 'Initech', Amount: 8000, StageName: 'Prospecting' },
];
// a function that returns a function
const amountAbove = (min) => (opp) => opp.Amount >= min;
const big = opportunities.filter(amountAbove(40000));
console.log(big.map((opp) => opp.Name));
// passing our own callback
function forEachOpen(list, callback) {
for (const item of list) {
if (!item.StageName.startsWith('Closed')) callback(item);
}
}
forEachOpen(opportunities, (opp) => console.log(`open: ${opp.Name}`));
[ 'Acme', 'Globex' ] open: Acme open: Initech
Event handler methods
In the template, onclick={handleSelect} passes a reference to the method – no parentheses, no arguments. The method receives the event. To know which item was clicked, put the id in a data-* attribute and read it from event.currentTarget.dataset:
<!-- productPicker.html -->
<template>
<template for:each={products} for:item="product">
<lightning-button key={product.id} label={product.name} data-id={product.id} data-price={product.price}
onclick={handleSelect}></lightning-button>
</template>
<p>{summary}</p>
</template>
// productPicker.js
import { LightningElement } from 'lwc';
export default class ProductPicker extends LightningElement {
products = [
{ id: 'p1', name: 'Laptop', price: 72000 },
{ id: 'p2', name: 'Monitor', price: 15500 },
];
selected = [];
handleSelect(event) {
const { id, price } = event.currentTarget.dataset; // data-* values are always strings
this.selected = [...this.selected, { id, price: Number(price) }];
}
get summary() {
const total = this.selected.reduce((sum, item) => sum + item.price, 0);
return `${this.selected.length} selected · total ${total}`;
}
}
[Laptop] [Monitor] 0 selected · total 0 › click “Laptop” [Laptop] [Monitor] 1 selected · total 72000 › click “Monitor” [Laptop] [Monitor] 2 selected · total 87500
data-pricebecomesdataset.price; a hyphenateddata-record-idbecomesdataset.recordId.- Use
currentTarget(the element the handler is on), nottarget(whatever was actually clicked – maybe an icon inside the button). - You can't write
onclick={handleSelect(product.id)}– templates don't call functions. data-* attributes are the LWC way to pass values.
Keep helpers pure
A pure function returns a result from its inputs and changes nothing outside itself. Move logic like "calculate the discount" out of the class into pure functions: they're easier to test with Jest and can be reused by other components (see ES modules).
// pure: same input, same output, no side effects
const discountFor = (amount, tier) => (tier === 'Gold' ? amount * 0.1 : tier === 'Silver' ? amount * 0.05 : 0);
// impure: depends on and changes outside state
let totalDiscountGiven = 0;
function applyDiscount(amount, tier) {
const d = discountFor(amount, tier);
totalDiscountGiven += d;
return amount - d;
}
console.log(discountFor(10000, 'Gold'), discountFor(10000, 'Bronze'));
console.log(applyDiscount(10000, 'Gold'), applyDiscount(5000, 'Silver'), totalDiscountGiven);
1000 0 9000 4750 1250
Practice
Try it yourself
A teammate writes
onclick={handleDelete(row.Id)}. What happens, and what should they write?Show answer
The template won't compile – templates can't call functions. Use
data-id={row.Id} onclick={handleDelete}and readevent.currentTarget.dataset.idin the handler.Write an arrow function
toPicklistOptionsthat turns['Hot', 'Warm']into[{ label: 'Hot', value: 'Hot' }, …].Show answer
const toPicklistOptions = (values) => values.map((v) => ({ label: v, value: v }));
Quick check
How does an arrow function return an object literal?
Wrap it in parentheses: () => ({ key: value }).
What type are values read from dataset?
Always strings.
target or currentTarget in a button handler?
currentTarget – the element the listener is attached to.
Summary
- Methods for the component, arrows for callbacks, declarations for shared helpers.
- Use defaults, rest parameters and options objects for flexible signatures.
- Handlers receive the event; pass values with data-* and read currentTarget.dataset.
- Pull business logic into pure functions so it can be tested and reused.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.