Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All articles
Development·May 3, 2026·18 min read

Lightning Web Components (LWC) for Beginners: Build Your First Component in 30 Minutes

What LWC is, why it replaced Aura, environment setup, Hello World, reactive properties, @wire and Apex, events, debugging cache issues, and quick-action LWCs.

Lightning Web Components for beginners — build your first component in 30 minutes

TL;DR

  • LWC is Salesforce's modern UI framework — built on standard web components, replacing Aura for new development.
  • You need: VS Code + Salesforce Extensions Pack, Salesforce CLI (sf), a sandbox or scratch org. ~10 minutes to set up if you've never done it.
  • Hello World is 3 files: HTML template, JS controller, XML metadata. We'll build it together.
  • The hardest part isn't writing LWC — it's the cache. Browser cache + platform cache + LWC component cache all interact in surprising ways.

If you've read about LWC but never built one, this article is the runway. We'll go from zero to a working, deployed Lightning Web Component in about 30 minutes — Hello World, reactive properties, Apex-bound data with @wire, events, and a finishing touch as a Quick Action.

What is LWC, and why did it replace Aura?

LWC is Salesforce's UI framework introduced in 2019, built on the Web Components standard. Standard means: <my-component> is a real browser primitive, not a Salesforce-specific abstraction. The same skills (web components, modern JavaScript, Shadow DOM) work outside Salesforce too.

Aura, by contrast, was Salesforce's first answer to "we need a modern UI framework" — built before Web Components were standardized. It worked, but it was Salesforce-specific, slow at scale, and required learning Aura-only patterns.

In 2026, the rule is: all new development is LWC. Aura is supported but won't get new investment. If you're maintaining an Aura org, plan a gradual migration as you touch components.

LWC vs Aura — what changed and why

Setting up your environment

Three tools.

  1. Visual Studio Code — free editor, https://code.visualstudio.com/
  2. Salesforce Extensions Pack — search the VS Code marketplace for "Salesforce Extension Pack" by Salesforce. Install.
  3. Salesforce CLI (sf) — install via npm:
    npm install -g @salesforce/cli
    sf --version
    

Then authorize an org:

sf org login web -a mydevhub --set-default-dev-hub
sf org login web -a mysandbox --set-default

The first command authorizes a Dev Hub (your production or trial org with Dev Hub enabled). The second authorizes a target org for development — typically a sandbox or scratch org.

Create a project:

sf project generate --name lwc-quickstart
cd lwc-quickstart

You're ready to build.

Hello World

Three files make a component. They share a directory name.

sf lightning generate component --type lwc -n helloWorld -d force-app/main/default/lwc

This generates:

force-app/main/default/lwc/helloWorld/
  ├── helloWorld.html         (template)
  ├── helloWorld.js           (controller)
  └── helloWorld.js-meta.xml  (metadata)

helloWorld.html

<template>
  <h1>Hello, {greeting}!</h1>
  <input value={greeting} oninput={handleChange} />
</template>

helloWorld.js

import { LightningElement } from 'lwc'

export default class HelloWorld extends LightningElement {
  greeting = 'World'

  handleChange(e) {
    this.greeting = e.target.value
  }
}

helloWorld.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
  <apiVersion>62.0</apiVersion>
  <isExposed>true</isExposed>
  <targets>
    <target>lightning__AppPage</target>
    <target>lightning__RecordPage</target>
    <target>lightning__HomePage</target>
  </targets>
</LightningComponentBundle>

Deploy and test:

sf project deploy start --source-dir force-app/main/default/lwc/helloWorld
sf org open

Drop the component on a Lightning App page from Setup → App Builder. Type into the input box; the heading updates. Reactive properties work because LWC re-renders the template when class properties change.

Reactive properties — the model

Every property on the class is reactive by default in LWC. Change this.greeting, the template re-renders. No useState, no setState — the framework watches.

For deep reactivity (objects, arrays), you need @track:

import { LightningElement, track } from 'lwc'

export default class TodoList extends LightningElement {
  @track items = []  // changes inside the array trigger re-render

  addItem() {
    this.items.push({ id: Date.now(), text: 'New' })  // works because @track
  }
}

Without @track, replacing the whole array (this.items = [...]) still triggers re-render, but mutating it (this.items.push(...)) does not.

Connecting to Apex with @wire

The pattern that makes LWC actually useful: pulling data from Salesforce.

First, an Apex class:

public with sharing class AccountController {
  @AuraEnabled(cacheable=true)
  public static List<Account> getTopAccounts(Integer howMany) {
    return [
      SELECT Id, Name, Industry, AnnualRevenue
      FROM Account
      ORDER BY AnnualRevenue DESC NULLS LAST
      LIMIT :howMany
    ];
  }
}

Two important keywords:

  • @AuraEnabled — exposes the method to the Lightning UI (LWC and Aura).
  • cacheable=true — caches the result client-side. Required for @wire. Read-only methods only.

Now the component:

import { LightningElement, wire } from 'lwc'
import getTopAccounts from '@salesforce/apex/AccountController.getTopAccounts'

export default class TopAccounts extends LightningElement {
  howMany = 5
  @wire(getTopAccounts, { howMany: '$howMany' }) accounts
}
<template>
  <template lwc:if={accounts.data}>
    <ul>
      <template for:each={accounts.data} for:item="a">
        <li key={a.Id}>{a.Name} — {a.Industry}</li>
      </template>
    </ul>
  </template>
  <template lwc:if={accounts.error}>
    <p>Error: {accounts.error.body.message}</p>
  </template>
</template>

@wire is reactive. The '$howMany' syntax says "re-call this function when howMany changes." Set this.howMany = 10 and the wire fires again.

@wire pattern: Apex → LWC, with cacheable read paths and imperative writes

Imperative Apex calls

@wire only works for read-only cacheable=true methods. For writes, call Apex imperatively:

import { LightningElement } from 'lwc'
import updateAccount from '@salesforce/apex/AccountController.updateAccount'

export default class AccountEditor extends LightningElement {
  async handleSave() {
    try {
      await updateAccount({ recordId: this.recordId, name: this.name })
      // success
    } catch (e) {
      console.error(e)
    }
  }
}

Imperative calls are fine for buttons and form submits. They aren't cached automatically — if you need caching, do it yourself.

Events — passing data up

Children fire events; parents listen.

Child component (childButton.js):

import { LightningElement } from 'lwc'

export default class ChildButton extends LightningElement {
  handleClick() {
    this.dispatchEvent(new CustomEvent('save', {
      detail: { id: 123 },
      bubbles: true,
      composed: true,
    }))
  }
}

Parent template:

<template>
  <c-child-button onsave={handleSave}></c-child-button>
</template>

Parent JS:

handleSave(e) {
  console.log('Got save event with id:', e.detail.id)
}

bubbles: true lets the event cross component boundaries. composed: true lets it cross the Shadow DOM boundary. Use both when you want the parent (or grandparent) to handle the event.

The cache problem (and how to fix it)

This is the LWC issue that costs beginners the most time.

You deploy a change. Refresh the browser. Nothing changes. What?

Three caches are stacked:

  1. Browser cache — JavaScript files served with long max-age headers.
  2. Platform cache — Salesforce's own component cache.
  3. LWC @wire cachecacheable=true data lives 15 minutes by default.

The three caches stacked between you and your component

Fixes:

  • For dev: open the org with ?sfdcRefresh=1 appended to the URL. Forces a refresh of platform-cached components.
  • Browser cache: disable cache in Chrome DevTools (Network tab → "Disable cache" while DevTools open).
  • @wire cache: call refreshApex(this.accounts) from lightning/uiRecordApi to bust the wire's cached result.
  • Hard reset: sf project deploy start --ignore-conflicts and a Cmd+Shift+R hard reload.

The two biggest pitfalls:

  • "I deployed but my changes aren't showing up." → Clear the browser cache, then add ?sfdcRefresh=1.
  • "I changed an Apex method but the LWC still shows old data." → Call refreshApex() or invalidate the wire on save.

Quick-action LWC — the most underused pattern

Adding lightning__RecordAction to your component's targets lets you launch it as a Quick Action from any record page.

helloWorld.js-meta.xml:

<targets>
  <target>lightning__RecordAction</target>
</targets>
<targetConfigs>
  <targetConfig targets="lightning__RecordAction">
    <actionType>ScreenAction</actionType>
  </targetConfig>
</targetConfigs>

Then in Setup → Object Manager → Account → Buttons, Links, and Actions → New Action, point at the LWC. Drop the action on the page layout.

Now from any Account, the user clicks your Quick Action; your LWC opens in a modal. Use this for inline editing, custom data-entry forms, complex calculation widgets — anything that's awkward in standard Salesforce UI.

Working with the record context

Your LWC often runs in the context of a record. Two key inputs:

import { LightningElement, api } from 'lwc'

export default class MyComponent extends LightningElement {
  @api recordId   // automatically populated when on a record page
  @api objectApiName // automatically populated to "Account", "Contact", etc.
}

@api makes a property public — it's how parents pass data in. @api properties on the targets lightning__RecordPage and lightning__RecordAction get populated automatically by the Lightning framework.

Combined with getRecord from lightning/uiRecordApi, you can fetch the current record without writing Apex:

import { LightningElement, wire, api } from 'lwc'
import { getRecord } from 'lightning/uiRecordApi'

const FIELDS = ['Account.Name', 'Account.Industry', 'Account.AnnualRevenue']

export default class AccountSummary extends LightningElement {
  @api recordId
  @wire(getRecord, { recordId: '$recordId', fields: FIELDS }) account
}

getRecord respects field-level security and sharing automatically. Use it instead of writing trivial Apex when possible.

Testing LWC

Salesforce ships Jest tests for LWC. Run them locally without deploying.

npm install --save-dev @salesforce/sfdx-lwc-jest
sf force lightning lwc test create -f force-app/main/default/lwc/helloWorld/helloWorld.js
sf force lightning lwc test run --component helloWorld

A simple test:

import { createElement } from 'lwc'
import HelloWorld from 'c/helloWorld'

describe('c-hello-world', () => {
  it('renders the greeting', () => {
    const el = createElement('c-hello-world', { is: HelloWorld })
    document.body.appendChild(el)
    const heading = el.shadowRoot.querySelector('h1')
    expect(heading.textContent).toBe('Hello, World!')
  })
})

LWC Jest tests are fast (no org connection) and great for component logic. Use them for pre-deploy validation; the full Salesforce test classes still cover Apex.

Connecting to Agentforce

In 2026, an LWC can be the surface where users interact with your agents. Two patterns:

  1. Embed the agent chat — drop the standard lightning-conversation-toolkit component to surface an Agentforce conversation in your LWC.
  2. Call agent Actions imperatively — your LWC dispatches a request to a server-side Apex method that calls an agent Action via the Atlas Reasoning Engine.

For most cases, the embed pattern is right. The imperative pattern is for power users who need agent capabilities inside a custom UI flow.

Common beginner mistakes

  • Forgetting cacheable=true on @wire Apex. You'll see a runtime error. Add it (and confirm the method is read-only).
  • Mutating an array without @track. Re-render won't fire. Either add @track or replace the array (this.items = [...this.items, newItem]).
  • Forgetting key on for:each items. Lightning will warn. Always include a unique key.
  • Hardcoding org-specific data. Use Custom Metadata or Custom Settings instead of magic strings.
  • Not handling the loading state. accounts.data is undefined initially. Handle it (<template lwc:if={accounts.data}>).
  • Skipping accessibility. ARIA labels, keyboard navigation, color contrast — all matter. Salesforce's design system (SLDS) gives you most of this for free if you use SLDS classes.
  • Cache confusion. When in doubt, append ?sfdcRefresh=1 and disable browser cache.

Frequently asked questions

Should I learn Aura first? No. New development is all LWC. Aura is for maintenance.

Can LWC and Aura coexist? Yes. LWC components can sit inside Aura apps and vice versa. You'll see hybrid orgs for years.

Do I need TypeScript? Salesforce announced TypeScript support for LWC in Spring '26 release. It's optional but recommended for larger projects. Plain JavaScript is still fully supported.

How does this connect to Flow? LWC components can be exposed as Flow Screens. Flow renders the LWC; the component reads/writes Flow variables via @api properties.

What's the relationship to Quick Actions? LWC + Quick Action is the modern pattern for custom actions on records. Replaces Aura quick actions and most legacy Visualforce action overrides.

Can LWC make external HTTP calls? Not directly — go through an Apex method that uses Named Credentials. Direct fetch from LWC works in some cases but bypasses CSP, CSRF, and integration governance.

If you only do one thing tonight: build Hello World end-to-end. The mechanics click after one full deploy cycle in a way no amount of reading does.

Share this article

Sources

Related dictionary terms

Keep reading