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.

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.
Setting up your environment
Three tools.
- Visual Studio Code — free editor, https://code.visualstudio.com/
- Salesforce Extensions Pack — search the VS Code marketplace for "Salesforce Extension Pack" by Salesforce. Install.
- 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.
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:
- Browser cache — JavaScript files served with long max-age headers.
- Platform cache — Salesforce's own component cache.
- LWC
@wirecache —cacheable=truedata lives 15 minutes by default.
Fixes:
- For dev: open the org with
?sfdcRefresh=1appended to the URL. Forces a refresh of platform-cached components. - Browser cache: disable cache in Chrome DevTools (Network tab → "Disable cache" while DevTools open).
@wirecache: callrefreshApex(this.accounts)fromlightning/uiRecordApito bust the wire's cached result.- Hard reset:
sf project deploy start --ignore-conflictsand 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:
- Embed the agent chat — drop the standard
lightning-conversation-toolkitcomponent to surface an Agentforce conversation in your LWC. - 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=trueon@wireApex. 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@trackor replace the array (this.items = [...this.items, newItem]). - Forgetting
keyonfor:eachitems. 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.dataisundefinedinitially. 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=1and 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.
What to read next
- LWC, Aura Component, Apex, Quick Action — the dictionary entries.
- Flow vs Apex 2026 — when LWC is the right answer vs Flow.
- Governor Limits Cheat Sheet — the Apex you call from your LWC shares the same limits.
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

Salesforce Data Model Explained: Objects, Records, Fields & Relationships (Beginner's Guide)
The complete beginner's guide to the Salesforce data model — objects, fields, all six relationship types, junction objects, record types, and Schema Builder. Worked examples included.

Salesforce Flow vs Apex in 2026: A Decision Matrix for Admins, Developers & Consultants
Flow vs Apex isn't a religious war anymore. Here's the 2026 decision matrix — capability gaps, governor limits, the 70/30 rule, and 12 worked scenarios with the right answer for each.
