LearnFrenzy
Sign in Take a free test

Functions, this and Closures · Lesson 4 of 25

Functions and Arrow Functions in LWC

5 min read Updated Nov 8, 2026 Saurabh Samir

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'));

Output

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

Output

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}`));

Output

[ '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}`;
    }
}

Result

[Laptop] [Monitor]
0 selected · total 0

› click “Laptop”
[Laptop] [Monitor]
1 selected · total 72000

› click “Monitor”
[Laptop] [Monitor]
2 selected · total 87500
  • data-price becomes dataset.price; a hyphenated data-record-id becomes dataset.recordId.
  • Use currentTarget (the element the handler is on), not target (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);

Output

1000 0
9000 4750 1250

Practice

Try it yourself

  1. 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 read event.currentTarget.dataset.id in the handler.

  2. Write an arrow function toPicklistOptions that 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.

Next lesson

Search LearnFrenzy

Esc

Quick links