Lightning Web Components
Build a searchable account list as a Lightning Web Component: set up VS Code and the Salesforce CLI, call Apex with @wire, deploy to your org and place the component on a Lightning page.
Lightning Web Components (LWC) is Salesforce's modern framework for building custom user interface. A component is a small, reusable piece of screen written with standard web technologies: HTML for the template, JavaScript (ES modules) for behaviour and CSS for style. Components can read Salesforce data, call Apex, and sit on any Lightning page.
In this tutorial you will set up VS Code, create an accountList component with a live search box, back it with an Apex method, deploy it, and place it on a page.
Prerequisites: a Developer Edition org, basic HTML and JavaScript (variables, functions, events), and *Apex basics* and *SOQL and SOSL*. Page placement uses what you learned in *Lightning App Builder and page layouts*. Deploying with the CLI is explained further in *Salesforce DX, sandboxes and deployment*.
How a component is built
Every component is a folder. The folder name is camelCase, and the files inside share that name:
Files in the accountList folder
| File | Purpose |
|---|---|
| accountList.html | The template: what is drawn. Must have a root <template> tag |
| accountList.js | The JavaScript class: data, getters, event handlers |
| accountList.css | Optional styles, scoped to this component only |
| accountList.js-meta.xml | Configuration: API version, where the component can be used (targets) |
In HTML you use a component with a dashed name: the folder accountList becomes <c-account-list> (c is the default namespace). In Salesforce's built-in library, components have names like lightning-card, lightning-input and lightning-button, called base components. They already follow Salesforce's design system (SLDS), so you rarely write CSS.
Step 1: set up your tools
- 1Install Visual Studio Code (free) and the Salesforce Extension Pack from the Extensions view.
- 2Install the Salesforce CLI (
sf) from the Salesforce developer site, and Node.js (LTS) if the pack asks for it. You also need Java for Apex features in the extension; the pack explains this. - 3Open a terminal and confirm the CLI works with
sf --version. - 4Create a project, authorise your org and open it in VS Code with the commands below.
sf project generate --name bean-lwc
cd bean-lwc
sf org login web --alias mydev --set-default
code .A browser window opens for you to log in to your Developer Edition org. The org is saved with the alias mydev. The last command opens the project in VS Code. You can instead use the command palette: SFDX: Create Project and SFDX: Authorize an Org.
The project has a force-app/main/default folder with subfolders for classes, lwc and more. Your code goes there.
Step 2: write the Apex method
A component cannot run SOQL by itself. It calls an Apex method. The method must be static, public or global, annotated with @AuraEnabled, and cacheable=true if the component only reads data (this lets the browser cache the result and allows the @wire service).
sf apex generate class --name AccountController --output-dir force-app/main/default/classesCreates AccountController.cls and AccountController.cls-meta.xml.
public with sharing class AccountController {
@AuraEnabled(cacheable=true)
public static List<Account> getAccounts(String searchKey) {
String term = String.isBlank(searchKey) ? '' : searchKey;
String pattern = '%' + term + '%';
return [
SELECT Id, Name, Industry, Phone
FROM Account
WHERE Name LIKE :pattern
WITH USER_MODE
ORDER BY Name
LIMIT 20
];
}
}Returns up to 20 accounts whose name contains the search text. A blank search returns the first 20. The bind variable keeps it safe from injection, and USER_MODE enforces the user's permissions.
Step 3: create the component
sf lightning generate component --name accountList --type lwc --output-dir force-app/main/default/lwcCreates the accountList folder with .html, .js and .js-meta.xml files. In VS Code you can use the command SFDX: Create Lightning Web Component instead.
<template>
<lightning-card title="Account search" icon-name="standard:account">
<div class="slds-var-p-around_medium">
<lightning-input
type="search"
label="Search accounts"
onchange={handleChange}>
</lightning-input>
<template lwc:if={hasResults}>
<ul class="slds-var-m-top_small">
<template for:each={accounts.data} for:item="acc">
<li key={acc.Id}>
<strong>{acc.Name}</strong>
<span> - {acc.Industry}</span>
</li>
</template>
</ul>
</template>
<template lwc:elseif={accounts.error}>
<p class="slds-text-color_error">Could not load accounts.</p>
</template>
<template lwc:else>
<p class="slds-var-m-top_small">No accounts found.</p>
</template>
</div>
</lightning-card>
</template>A card with a search input and a list. lwc:if, lwc:elseif and lwc:else choose what to draw. Each item in for:each needs a unique key. onchange={handleChange} connects the input to a JavaScript method.
import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
const DELAY = 300;
export default class AccountList extends LightningElement {
searchKey = '';
delayTimeout;
@wire(getAccounts, { searchKey: '$searchKey' })
accounts;
get hasResults() {
return this.accounts.data && this.accounts.data.length > 0;
}
handleChange(event) {
const value = event.target.value;
window.clearTimeout(this.delayTimeout);
this.delayTimeout = window.setTimeout(() => {
this.searchKey = value;
}, DELAY);
}
}The wire service calls getAccounts and puts the result in accounts as { data, error }. The '$searchKey' prefix makes the parameter reactive: when searchKey changes, the wire re-runs. The timeout waits 300 ms after the user stops typing so you do not call Apex on every key press.
Four ideas in that file are worth learning well:
Fields are reactive. In modern LWC, fields declared in the class re-render the template when you assign a new value (this.searchKey = value). Changing a property inside an object or array that you mutate in place is not tracked; assign a new object or array instead.
Getters such as hasResults hold logic that templates cannot (templates allow only simple property references, no expressions like data.length > 0).
`@wire` connects a function or property to a data source and handles loading, caching and refresh for you.
Events flow through the DOM. The onchange attribute listens to the standard change event from the input.
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Account Search</masterLabel>
<targets>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>isExposed true and the targets make the component show up in Lightning App Builder for App pages and Home pages. Keep the apiVersion the generator wrote for you if it differs from 62.0.
Step 4: deploy and use it
- 1In the terminal, deploy the class and the component:
sf project deploy start --source-dir force-app --target-org mydev. - 2In your org click the gear icon > Setup, search Lightning App Builder and click New. Choose Home Page and finish the wizard.
- 3Find Account Search under Custom in the component list, drag it onto the canvas, Save and Activate it as the org default (or for your app).
- 4Open the Home page, type part of an account name, and watch the list update.
The Home page shows a card named Account search with a search box. Typing 'ed' after a short pause lists accounts such as 'Edge Communications' (from the sample data) with their industries. Clearing the box lists the first 20 accounts.If the component is missing from the builder, check that isExposed is true and the target matches the page type, and re-deploy. If it says the Apex method is not found, check that the class was deployed and the names in the import match exactly: @salesforce/apex/ClassName.methodName.
Passing data between components
Components talk in two directions. A parent sends data down to a child with public properties marked @api. A child sends messages up with a custom event that the parent listens for.
// accountCard.js (child)
import { LightningElement, api } from 'lwc';
export default class AccountCard extends LightningElement {
@api account;
handleSelect() {
this.dispatchEvent(new CustomEvent('select', {
detail: { accountId: this.account.Id }
}));
}
}@api account lets the parent set the property. dispatchEvent sends an event named select with the Id inside detail.
<!-- inside the parent template -->
<c-account-card account={acc} onselect={handleSelect}></c-account-card>Sets the child's account property and listens for its select event. Event names are lowercase without hyphens.
Other things you will use
| Need | Tool |
|---|---|
| Show or edit one record without Apex | lightning-record-form, lightning-record-edit-form, or the lightning/uiRecordApi wire adapters (Lightning Data Service) |
| Call Apex on a button click | Import the method and call it as a promise: getAccounts({ searchKey }).then(...).catch(...) |
| Show a toast message | Import ShowToastEvent from lightning/platformShowToastEvent |
| Navigate to a record | NavigationMixin from lightning/navigation |
| Run code when the component loads | connectedCallback() lifecycle method |
| Unit test the JavaScript | Jest, with the @salesforce/sfdx-lwc-jest package |
Avoid loops in lifecycle hooks
Never call an imperative Apex method from renderedCallback without a guard: it runs after every render and can create an endless loop of calls. Use connectedCallback for load-time work, or @wire for reactive reads.
LWC versus Aura. Aura is the older component framework. Salesforce recommends LWC for new work, and the two can live together on a page. You will see Aura in older orgs and some interview questions.
How this comes up in interviews. Be ready to explain: the files in a component; @api, @wire and @track (the last is mostly unnecessary now for primitive fields and plain objects); wire versus imperative Apex; why cacheable=true is needed for wire; how a child sends data to a parent (custom events); the lifecycle hooks connectedCallback and renderedCallback; and LWC versus Aura.
You are an expert in Salesforce Lightning Web Components. My component does not behave as expected.
Expected: [what should happen]
Actual: [what happens, plus any browser console or deployment error]
HTML:
[paste html]
JavaScript:
[paste js]
Apex (if any):
[paste apex]
Explain the cause in plain English, then show the corrected code. Mention whether this is a wire or imperative Apex problem.What to expect: A good answer pins the issue to one cause (naming mismatch, missing cacheable=true, template using an expression, non-reactive mutation) and shows corrected code. Check the browser developer console too: LWC errors often appear there.
Try it: Extend the component
Add a button labelled Clear to accountList that empties the search box and shows the default list again. Then display each account's Phone under its name, and sort results so accounts with a blank industry appear last.
Key takeaways
- A component is a folder of HTML, JavaScript, optional CSS and a meta file; folder accountList is used as c-account-list.
- Use @wire with a cacheable @AuraEnabled Apex method for reactive reads, and the '$property' syntax to re-run when a value changes.
- Templates only reference properties and getters; put logic such as conditions and formatting in JavaScript getters.
- Parents pass data down with @api properties; children send messages up with CustomEvent.
- Expose a component to App Builder with isExposed and targets in the meta file, then deploy with sf project deploy start.
Check yourself
Finishing the quiz marks the lesson as done.
1. Your folder is named accountList. How do you use it in another component's HTML?
2. Why does the Apex method used by @wire need cacheable=true?
3. What does the '$searchKey' syntax in @wire(getAccounts, { searchKey: '$searchKey' }) do?
4. A child component needs to tell its parent that a button was clicked. What should it use?
5. A component does not appear in Lightning App Builder after deployment. What is the most likely cause?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.