Glumes ITSalesforce Ecosystem Experts
Back to Blog
Development

Lightning Web Components: A Modern Developer's Guide

Glumes TeamOctober 26, 202512 min read

LWC in 30 seconds

Standards-based Web Components with reactive rendering, shadow DOM, and a compiler that ships to modern browsers. If you know ES modules + class syntax, you know 80% of LWC.

Anatomy

force-app/main/default/lwc/dealCard/
├─ dealCard.js          # controller
├─ dealCard.html        # template
├─ dealCard.css         # scoped styles
├─ dealCard.js-meta.xml # visibility, targets, design attributes

Reactive data with @wire

import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import NAME from '@salesforce/schema/Opportunity.Name';
import AMOUNT from '@salesforce/schema/Opportunity.Amount';

export default class DealCard extends LightningElement {
  @api recordId;

  @wire(getRecord, { recordId: '$recordId', fields: [NAME, AMOUNT] })
  opp;

  get name()   { return getFieldValue(this.opp.data, NAME); }
  get amount() { return getFieldValue(this.opp.data, AMOUNT); }
}

Reactive property = leading $. When recordId changes, the wire re-invokes.

Calling Apex

public with sharing class DealCardController {
  @AuraEnabled(cacheable=true)
  public static List<Opportunity> topDeals(Id accountId) {
    return [SELECT Id, Name, Amount FROM Opportunity
            WHERE AccountId = :accountId AND IsClosed = false
            ORDER BY Amount DESC LIMIT 5 WITH USER_MODE];
  }
}
import topDeals from '@salesforce/apex/DealCardController.topDeals';

@wire(topDeals, { accountId: '$recordId' })
deals;

cacheable=true lets Lightning Data Service cache the response — huge for list components.

Composition, not inheritance

Slots > subclasses. Build a <c-panel> with named slots:

<template>
  <div class="panel">
    <header><slot name="title"></slot></header>
    <div class="body"><slot></slot></div>
    <footer><slot name="actions"></slot></footer>
  </div>
</template>

Consume:

<c-panel>
  <h2 slot="title">Deal Health</h2>
  <c-health-score record-id={recordId}></c-health-score>
  <lightning-button slot="actions" label="Refresh"></lightning-button>
</c-panel>

Events between components

  • Parent → child: @api public properties + methods
  • Child → parent: this.dispatchEvent(new CustomEvent('select', { detail: id }))
  • Sibling ↔ sibling: Lightning Message Service (LMS)
import { publish, MessageContext } from 'lightning/messageService';
import DEAL_SELECTED from '@salesforce/messageChannel/DealSelected__c';
@wire(MessageContext) messageContext;
handleSelect(id) { publish(this.messageContext, DEAL_SELECTED, { id }); }

Testing with Jest

import { createElement } from 'lwc';
import DealCard from 'c/dealCard';
import { getRecord } from 'lightning/uiRecordApi';

const mockRecord = require('./data/getRecord.json');

test('renders opportunity name', async () => {
  const el = createElement('c-deal-card', { is: DealCard });
  el.recordId = '006xx0000000001';
  document.body.appendChild(el);

  getRecord.emit(mockRecord);
  await Promise.resolve();

  expect(el.shadowRoot.querySelector('h2').textContent).toBe('Acme Renewal');
});

Aim for 80% branch coverage with real assertions, not snapshot-only tests.

Performance rules

  • @wire with cacheable Apex — never imperative fetch in a getter
  • Use @track only on non-reactive fields you mutate (arrays/objects)
  • Chunk long lists with Lightning Datatable + Infinite Loading
  • Lighthouse audit each community-facing LWC — LCP < 2.5s
LWCSalesforceDevelopment

Related articles