This page is organized from state surface to behavior surface: property decorators first, then public methods, then internal function patterns, and finally computed properties for template-facing values.

Property Decorators

Private Property Conventions

Standard: Underscore Prefix (_) for Backing Properties

Use the underscore prefix (_) convention exclusively for backing properties behind @api getter/setter pairs. The _ prefix is not a general "private" marker -- it specifically signals a property that stores the internal value for a public API.

Properties without @api are already private by default in LWC, so the _ prefix is only needed when a corresponding public getter/setter exists.

Do not use _ on methods. The absence of @api is the sole indicator that a method is private. See Methods for naming conventions.

Example:

export default class ExampleParent extends LightningElement {
    // Backing property for the public customClass API
    _customClass = 'default-class';

    @api get customClass() {
        return this._customClass;
    }

    set customClass(value) {
        this._customClass = value || 'default-class';
    }

    // Private reactive property (no underscore, no @api)
    isLoading = false;

    // Private method (no underscore, no @api)
    setColorClasses() {
        // ...
    }
}

Scope rules for _ properties:

  • Only reference them within the same JavaScript file
  • Never access them from templates, other files, or parent/child components
  • Expose their values through public getters/setters instead

Why not # (ES2022 private fields)?

LWC already treats every non-@api member as private to consumers. The framework defines encapsulation through @api, so # private fields do not add meaningful protection for typical component work.

Beyond the lack of benefit, # fields interact poorly with LWC's architecture and are not recommended:

  • Templates are compiled separately from the component class. Because # names are lexically scoped to the class body, the compiled template code cannot bind to them the way it binds to ordinary properties and getters.
  • The LWC component instance and the custom element are separate objects. The framework proxies @api members to the host element at runtime but has no mechanism to intercept # private members.
  • Code that uses # may compile in narrow cases but risks surprising failures around template access, inheritance, and tooling that expects conventional LWC patterns.

This is an ongoing architectural discussion in the LWC repository.

@api Decorator

Usage: Use @api to expose public properties and methods that can be set by a parent component or used in the component configuration.

Guidelines:

  • Use @api for properties that need to be configurable from outside the component
  • Use @api for methods that need to be callable from parent components
  • Properties decorated with @api are reactive by default
  • Use getters/setters when transformation or validation is needed

Direct @api Properties:

Use direct @api properties when no transformation or validation is needed:

// Good: Simple property, no transformation needed
@api recordId;
@api label = 'Default Label';
@api disabled = false;
@api iconName;

@api Getters/Setters:

Use getters/setters when you need to transform, validate, or perform side effects:

// Good: Transformation needed
// Note: _customClass is private - only accessed within this file
@api get customClass() {
    return this._customClass;
}

set customClass(value) {
    this._customClass = value || DEFAULT_CLASS;
    this.classList.add(...this._customClass.split(' '));
}

// Good: Validation needed
// Note: _borderRadius is private - only accessed within this file
@api get borderRadius() {
    return this._borderRadius;
}

set borderRadius(value) {
    if (value && isValidStyle(value, 'borderRadius')) {
        this._cssVariables['--example-child-border-radius'] = value;
    }
    this._borderRadius = value;
}

Important: The underscore-prefixed properties (_customClass, _borderRadius) are private implementation details. They should never be referenced in:

  • Component HTML templates (use the public getter/setter instead)
  • Other JavaScript files
  • Parent or child components

Bad Example (Accessing Private Property in Template):

<!-- Bad: Accessing private property directly -->
<div class={_customClass}>Content</div>

Good Example (Using Public API in Template):

<!-- Good: Using public property/getter -->
<p class={customClass}>Content</p>
// Good: Template uses public API, private property stays internal
@api get customClass() {
    return this._customClass;
}

@track Decorator

Usage: Use @track when you need the engine to observe in-place mutations of a plain object or array held in a field (nested properties, push, index assignment, and so on). Starting in API 48 / Spring '20, class fields are reactive by default for reassignment (for example this.isLoading = true), so @track is unnecessary for primitives and for object/array fields when you always replace the whole value.

Guidelines:

  • API 48 / Spring '20 and later: Fields on the component class react to new assignments; primitives are compared with ===.
  • Plain objects and arrays without @track: Only reassigning the field triggers an update. Mutating this.config.enabled or this.items.push(item) does not rerender unless the field is @track or you assign a new object/array to the field.
  • Prefer immutable updates (new object/array reference) for clarity and predictable rerenders; use @track when you intentionally mutate in place.
  • @track does not deeply observe Date, Map, Set, or class instances
    • only plain {} and []. After changing those types, reassign the field
      or expose a primitive/getter the template can read. The runtime may log a non-trackable object warning when @track is applied to an unsupported value.

See: Reactivity for fields, objects, and arrays.

When to Use @track:

// Good: in-place mutation of a plain object is observed
@track config = {
    enabled: true,
    size: 'medium'
};

// Good: in-place array mutation is observed (immutable updates are still preferred)
@track items = [];

// Unnecessary since API 48 / Spring '20 — reassignment is enough for primitives
// @track isLoading = false;

Preferred Pattern (immutable updates — works with or without @track):

// Without @track: reassign the field so nested changes rerender
updateConfig(newValue) {
    this.config = {
        ...this.config,
        enabled: newValue
    };
}

addItem(item) {
    this.items = [...this.items, item];
}

@wire Decorator

Usage: Use @wire to read Salesforce data reactively. The wire service provides data to the component automatically.

Guidelines:

  • Use @wire for reactive data that should update automatically
  • Wire adapters include: getRecord, getRecordUi, getObjectInfo, getPicklistValues, Apex methods, and message channels
  • Wire properties are read-only - don't assign to them directly
  • Handle wire errors appropriately

Examples:

import { wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import { MessageContext } from 'lightning/messageService';
import getAccountData from '@salesforce/apex/AccountController.getAccountData';

// Wire to Lightning Data Service
@wire(getRecord, { recordId: '$recordId', fields: ACCOUNT_FIELDS })
wiredAccount({ error, data }) {
    if (data) {
        this.account = data;
        this.error = undefined;
    } else if (error) {
        this.error = error;
        this.account = undefined;
    }
}

// Wire to Apex method
@wire(getAccountData, { accountId: '$recordId' })
wiredAccountData({ error, data }) {
    if (data) {
        this.accountData = data;
    } else if (error) {
        this.handleError(error);
    }
}

// Wire to Message Channel
@wire(MessageContext)
messageContext;

See: Data and Platform for detailed wire adapter patterns.

Tracked vs Reactive Properties

Class fields and assignment (API 48 / Spring '20 and later):

Fields declared on the component class participate in reactivity when you assign a new value. Primitives and whole-object/array replacement do not require @track.

// Rerender when values change if the template (or a getter used by it) reads them
@api recordId;
label = 'Default';
isLoading = false;
items = []; // element changes: use @track or immutable list updates

When the UI rerenders:

A rerender runs when a change affects something the template read in the previous render cycle—either a field referenced in the template or data reached through a getter the template uses. With @track, mutations that do not affect rendered paths may still be ignored.

Details: Reactivity for fields, objects, and arrays.

What is not reactive:

  • Local variables and other values scoped inside methods (not component class fields)
  • Static properties on the class
  • Expando properties added to objects at runtime

Best practice:

  • Prefer new object/array references over in-place mutation unless @track is a deliberate choice.
  • Use @track only when relying on deep mutation of a plain object or array.
  • For Date, Map, Set, or custom class instances, reassign the field after meaningful change instead of expecting deep tracking.

Public Methods

Public methods are the component's explicit behavior contract with parent components.

When to Expose Public Methods

Expose @api methods when:

  1. Parent components need to control child behavior - Focus, click, reset, etc.
  2. Component needs to be controlled externally - Show/hide, enable/disable programmatically
  3. Utility methods that parent components need - Validation, data retrieval, etc.

Examples:

// Good: Focus method for parent to control focus
@api focus() {
    const button = this.template.querySelector('button');
    if (button) {
        button.focus();
    }
}

// Good: Click method for parent to trigger click
@api click() {
    const button = this.template.querySelector('button');
    if (button) {
        button.click();
    }
}

// Good: Reset method for parent to reset component state
@api reset() {
    this.value = '';
    this.error = undefined;
    this.isDirty = false;
}

// Good: Validation method for parent to check validity
@api validate() {
    if (!this.value) {
        this.error = 'Value is required';
        return false;
    }
    this.error = undefined;
    return true;
}

Public Method Patterns

When Connection Checks Are Needed:

For synchronous public @api methods, connection checks are typically not necessary because:

  • Parent components can only call child methods when the child is connected
  • LWC ensures components are connected before they can be interacted with
  • Event handlers are only called when components are connected

Connection checks ARE needed for:

  1. Async operations - Promises, setTimeout, setInterval callbacks that might complete after disconnection
  2. Wire adapter callbacks - Can sometimes fire after disconnection in edge cases (though LWC usually handles this)

Examples:

// Not needed: Synchronous public method (component is inherently connected)
@api focus() {
    const button = this.template.querySelector('button');
    if (button) { // Check element existence, not connection
        button.focus();
    }
}

// Needed: Async operation that might complete after disconnection
async loadData() {
    const data = await fetchData();
    if (this._connected) { // Check needed - component might be disconnected
        this.data = data;
    }
}

// Track connection state (only needed if you use connection checks)
connectedCallback() {
    this._connected = true;
}

disconnectedCallback() {
    this._connected = false;
}

Return Values:

Public methods can return values for parent components:

// Good: Return validation result
@api validate() {
    if (!this.value) {
        return { isValid: false, error: 'Value is required' };
    }
    return { isValid: true, error: undefined };
}

// Good: Return current state
@api getState() {
    return {
        value: this.value,
        isValid: this.isValid,
        isDirty: this.isDirty
    };
}

Public Method Naming

  • Use verb-based names: focus(), click(), reset(), validate(), getState()
  • Be descriptive about what the method does
  • Avoid generic names like doSomething() or handle()

Function and Method Patterns

In LWC classes, the meaningful choice is between regular methods (prototype) and class fields that hold arrow functions (often called "arrow methods"). Arrow functions used in modules, .map() / .then(), or other inline callbacks are the same language feature in a different place. Prefer regular methods as the default; use arrow class fields when you pass the function as a value and the caller will not preserve this.

LWC templates: Handlers like onclick={handleSave} invoke your method with the correct component instance. You do not need an arrow class field for template wiring alone.

Regular class methods

Syntax: methodName() { ... } (no arrow on the class body).

  • Where they live: On the class prototype - one function shared by all instances.
  • this: Set by how the function is called (LWC sets it correctly for template-bound handlers).
  • Use for: Default component logic, helpers called from other methods, and anything the framework must recognize by name.
  • Required for: constructor, lifecycle hooks (connectedCallback, disconnectedCallback, etc.), and @api methods (must be prototype methods).
import { api, LightningElement } from 'lwc';

export default class Example extends LightningElement {
    connectedCallback() {
        this.initialize();
    }

    initialize() {
        // Internal logic
    }

    handleSave() {
        if (!this.isValid()) {
            return;
        }
        this.persist();
    }

    isValid() {
        return true;
    }

    persist() {
        // ...
    }

    @api
    refreshData() {
        // @api must be a regular method
    }
}

Arrow class fields ("arrow methods")

Syntax: methodName = () => { ... } (class field whose value is an arrow function).

  • Where they live: A new function per instance (slightly more memory than prototype methods; usually fine unless you have very large instance counts).
  • this: Lexical - always the component instance, even if the function is stored and called later (e.g. from an object map or timer).
  • Use when: You pass this.someMethod as a callback and the consumer calls it without a bound this, or you keep handlers in a map keyed by name.
export default class Example extends LightningElement {
    handleAction(event) {
        const actions = {
            save: this.save,
            remove: this.remove
        };
        actions[event.detail.action]?.();
    }

    save = () => {
        // `this` is still the component when invoked from `actions`
    };

    remove = () => {
        // same
    };
}

Avoid arrow class fields for bulk reusable logic that does not need lexical this; use a regular method instead so the function stays on the prototype.

Arrow functions in modules and callbacks

Outside the class, arrow functions are normal JavaScript: shared utilities (named const exports), array / promise callbacks, and inline callbacks where you want lexical this from the enclosing method (e.g. setTimeout(() => this.tick(), 1000)).

// c/utils.js — stateless helpers
export const formatDate = (dateString) => {
    const options = { year: 'numeric', month: 'short', day: 'numeric' };
    return new Date(dateString).toLocaleDateString(undefined, options);
};
import { LightningElement } from 'lwc';

export default class Example extends LightningElement {
    accounts = [];

    handleFilter() {
        const names = this.accounts
            .filter((account) => account.Revenue > 1_000_000)
            .map((account) => account.Name);
        // use names...
    }

    startTimer() {
        setTimeout(() => {
            // lexical `this` from enclosing method scope
            console.log(this.accounts?.length);
        }, 1000);
    }
}

Prefer explicit parameters or rest (...args) instead of the legacy arguments object in new code.

At a glance

Topic Regular method Arrow class field
this Dynamic (caller sets it; LWC sets it for template handlers) Lexical (always instance)
Storage Prototype (shared) Per instance
@api / lifecycle Required Not valid for these
Typical use Default for component logic Passing methods into maps, timers, or APIs that do not bind this

Reference:

Computed Properties

After defining public/private behavior, use computed properties to shape values consumed by templates.

Getters vs Methods

Usage: Use getters for computed properties that are used in templates. Use methods for computed values that require parameters or are used in JavaScript.

Getters:

// Good: Use getter for template-bound computed property
get fullName() {
    return `${this.firstName} ${this.lastName}`;
}

get isDisabled() {
    return this.loading || !this.isValid;
}

get displayValue() {
    return this.value || 'N/A';
}

Methods:

// Good: Use method when parameter is needed
formatValue(value) {
    return value ? value.toUpperCase() : '';
}

calculateTotal(items) {
    return items.reduce((sum, item) => sum + item.price, 0);
}

When to Use Getters

Use getters when:

  • Property is used in the template
  • Property is computed from other reactive properties
  • Property doesn't require parameters
  • Property should be reactive to changes in dependencies

Examples:

// Good: Getter used in template
get buttonClass() {
    return `slds-button ${this.variant ? `slds-button_${this.variant}` : ''}`;
}

get isVisible() {
    return this.items && this.items.length > 0;
}

get displayText() {
    return this.text || this.defaultText || 'No text';
}

Getter Best Practices

  • Keep getters simple and fast
  • Avoid side effects in getters
  • Don't modify component state in getters
  • Cache expensive computations if needed
  • Use getters for template-bound computed properties

Change History

Version 1.0 - 2026-03-26

  • Added initial version history tracking for this document.

Last Updated: 2026-03-26 Version: 1.0