sf-lwc-development
Skill2 starsChanged 2 months agoMITView on GitHub
Covers building Lightning Web Components on the Salesforce platform - bundle anatomy and js-meta.xml targets/targetConfigs, the reactivity model, @api/@wire/@track decorators, Lightning Data Service and imperative Apex, CustomEvent and Lightning Message Service communication, lifecycle hooks, template directives (lwc:if, for:each, iterator, lwc:ref, lwc:dom="manual", slots), SLDS styling hooks, shadow vs light DOM, NavigationMixin, toasts, labels and i18n, static resources and third-party JS, Lightning Web Security constraints, and performance/accessibility rules. Use this skill whenever work touches force-app/**/lwc/**, a *.js-meta.xml file, a wire adapter such as getRecord or graphql, an @AuraEnabled(cacheable=true) method consumed from JavaScript, refreshApex, a LightningMessageChannel, or when a component must be exposed to a Lightning App Page, Record Page, Flow screen, quick action, or Experience Cloud site.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- jiten-singh-shahi/salesforce-claude-codetheir own version, first seen Apr 10, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- jiten-singh-shahi/salesforce-claude-code★ 15same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository34 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- sf-agentforce-developmentSkill
- sf-apex-developmentSkill
- sf-apex-testingSkill
- sf-async-apex-patternsSkill
- sf-cli-operationsSkill
- sf-code-analyzer-qualitySkill
- sf-data-cloudSkill
- sf-data-managementSkill
- sf-debugging-logsSkill
- sf-deployment-strategiesSkill
- sf-fflib-domain-service-uowSkill
- sf-fflib-foundationsSkill
- sf-fflib-operationsSkill
- sf-fflib-selector-layerSkill
- sf-fflib-testingSkill
- sf-flow-automationSkill
- sf-governor-limitsSkill
- sf-integration-patternsSkill
- sf-local-developmentSkill
- sf-lwc-jest-testingSkill
- sf-minimal-changeSkill
- sf-org-security-auditSkill
- sf-packaging-releaseSkill
- sf-post-deploy-verificationSkill
- sf-process-builder-migrationSkill
- sf-project-structureSkill
- sf-scratch-orgs-sandboxesSkill
- sf-security-modelSkill
- sf-setup-automationSkill
- sf-soql-sosl-optimizationSkill
- sf-technical-debt-auditSkill
- sf-ui-test-automationSkill
- sf-workflow-orchestrationSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 132
Never mutate an @api property from inside the component.
- Very specific: line 347
Never hardcode user-facing strings: import custom labels via @salesforce/label/c.Name and locale data via @salesforce/i18n/*.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Lightning Web Components Development
When to use
| Situation | Use this skill |
Creating or editing anything under force-app/main/default/lwc/ | Yes |
Choosing between @wire, imperative Apex, and Lightning Data Service | Yes |
| Component must appear in Lightning App Builder, Flow Builder, or Experience Builder | Yes |
| Writing the Apex controller behind the component | Skill sf-apex-development (this skill covers only the JS side of the contract) |
| Writing Jest tests for the component | Skill sf-lwc-jest-testing |
| Previewing the component without deploying | Skill sf-local-development |
| ESLint/PMD/Code Analyzer findings on LWC JS | Skill sf-code-analyzer-quality |
| Generating a component, spec or migration with Salesforce DX MCP tools or Agentforce Vibes LWC skills | Yes - references/mcp-and-skills.md, then the same gates apply |
LWC is the owner slice of sf-lwc-engineer in wave 1: lwc/ and aura/ only. Apex signatures the
component consumes are fixed in wave 0 and recorded in .vibeforce/state/contract.md.
Decision table
| Need | Mechanism | Cacheable | Refresh path |
| Read one record, fields known at build time | @wire(getRecord, {recordId, fields}) | Yes (LDS) | Automatic; notifyRecordUpdateAvailable() after out-of-band writes |
| Read several related objects in one round trip | @wire(graphql, {query, variables}) | Yes (LDS) | refreshGraphQL / re-evaluate variables |
| Read object metadata or picklists | getObjectInfo, getPicklistValues | Yes (LDS) | Automatic |
| Read a query/aggregate LDS cannot express | @wire(apexMethod) with @AuraEnabled(cacheable=true) | Yes (client cache) | refreshApex(this.wiredResult) |
| Write one record, standard fields | createRecord / updateRecord / deleteRecord (lightning/uiRecordApi) | n/a | LDS updates wires automatically |
| Write with server-side business logic | imperative Apex (@AuraEnabled, never cacheable=true) | No | await notifyRecordUpdateAvailable([{recordId}]) |
| Form UI with zero JS | lightning-record-form / lightning-record-edit-form | n/a | onsuccess event |
| Fire-and-forget action on user click | imperative Apex in an async handler | No | n/a |
Rule: a method annotated @AuraEnabled(cacheable=true) cannot perform DML; it is the only shape
@wire accepts. Any DML path must be a separate non-cacheable @AuraEnabled method.
Core patterns
1. Bundle anatomy and configuration
force-app/main/default/lwc/accountCard/
accountCard.js default-exported class extending LightningElement
accountCard.html template (optional only for service components)
accountCard.css scoped styles (optional)
accountCard.js-meta.xml required; controls exposure and builder configuration
__tests__/accountCard.test.js Jest suite (gate: vf-check jest)<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>67.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Account Card</masterLabel>
<description>Shows key account fields with an escalate action.</description>
<targets>
<target>lightning__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightningCommunity__Default</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage">
<property name="showEscalate" type="Boolean" label="Show Escalate Button" default="true"/>
<objects>
<object>Account</object>
</objects>
</targetConfig>
<targetConfig targets="lightningCommunity__Default">
<property name="recordId" type="String" label="Record Id"/>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>apiVersion comes from config/vibe-force.defaults.json (67.0). Beginning in Spring '25 every
component must carry an apiVersion tag to be saved back to Salesforce. isExposed=false hides the
component from all builders; exposure additionally requires at least one <target>. Full target list:
references/component-reference.md.
2. Reactivity: fields, not state objects
import { LightningElement, track } from 'lwc';
export default class ReactivityExample extends LightningElement {
// Reactive by default: reassignment of a field re-renders if the field is used in the template
count = 0;
filters = { status: 'Open' };
@track rows = []; // @track only for mutation-in-place of objects/arrays
get label() { // getters are the supported way to compute template values
return `${this.count} open`;
}
mutate() {
this.count = 1; // rerender
this.filters.status = 'Closed'; // NO rerender: field not reassigned
this.filters = { ...this.filters, status: 'Closed' }; // rerender
this.rows.push({ id: '1' }); // rerenders only because of @track
}
}Reassigning a field to an ===-equal primitive is not a mutation and does not rerender. Prefer
immutable replacement ({...obj}, [...arr]) over @track; reserve @track for deep mutation you
genuinely cannot avoid. @track and @api on the same property is a compile error (LWC1093).
3. Public API: @api properties and methods
import { LightningElement, api } from 'lwc';
export default class Badge extends LightningElement {
@api recordId; // set by the record page container
@api variant = 'neutral';
_rows = [];
@api
get rows() { return this._rows; }
set rows(value) { this._rows = Array.isArray(value) ? [...value] : []; } // normalize on the way in
@api focusFirst() { // public method, called by parent via querySelector
this.refs.firstInput?.focus();
}
}Never mutate an @api property from inside the component; the owner owns the value. Public property
setters run before connectedCallback() on initial render and again on every reassignment by the
owner.
4. Wire service and refresh
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import RATING_FIELD from '@salesforce/schema/Account.Rating';
import getOpenCases from '@salesforce/apex/AccountCaseController.getOpenCases';
import { refreshApex } from '@salesforce/apex';
export default class AccountCard extends LightningElement {
@api recordId;
wiredCases;
error;
@wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD, RATING_FIELD] })
account;
@wire(getOpenCases, { accountId: '$recordId' })
handleCases(result) {
this.wiredCases = result; // keep the whole result for refreshApex
const { data, error } = result;
this.error = error ? this.reduce(error) : undefined;
}
get accountName() {
return getFieldValue(this.account.data, NAME_FIELD);
}
async refresh() {
await refreshApex(this.wiredCases);
}
reduce(error) {
return error?.body?.message ?? error?.body?.pageErrors?.[0]?.message ?? 'Unknown error';
}
}$recordId makes the configuration dynamic: when recordId changes, the adapter re-provisions.
refreshApex takes the value provisioned by an Apex @wire (the whole result object, not
result.data). Never mutate a wire configuration property inside renderedCallback(); that is an
infinite-loop generator.
5. Imperative Apex and error shape
import escalate from '@salesforce/apex/CaseEscalationService.escalate';
import { notifyRecordUpdateAvailable } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
async handleEscalate() {
this.busy = true;
try {
await escalate({ caseId: this.recordId, reason: this.reason });
await notifyRecordUpdateAvailable([{ recordId: this.recordId }]);
this.dispatchEvent(new ShowToastEvent({
title: 'Escalated', message: 'Case escalated to tier 2.', variant: 'success'
}));
} catch (error) {
this.dispatchEvent(new ShowToastEvent({
title: 'Escalation failed', message: this.reduce(error), variant: 'error', mode: 'sticky'
}));
} finally {
this.busy = false;
}
}Apex errors reach JavaScript as { body: { message } } for AuraHandledException, and as
{ body: { pageErrors: [], fieldErrors: {}, output: {...} } } for DML/record errors from LDS. Always
reduce defensively - see references/data-access-patterns.md for a complete reduceErrors helper.
6. Events: child to parent, and cross-tree
// child: selector.js
handleSelect(event) {
this.dispatchEvent(new CustomEvent('rowselect', {
detail: { recordId: event.currentTarget.dataset.id } // primitives only
}));
}<!-- parent -->
<c-selector onrowselect={handleRowSelect}></c-selector>Defaults are bubbles: false, composed: false: the event stops at the component boundary, which is
what you want for parent/child. Use { bubbles: true, composed: true } only for genuinely
application-level events, and never put mutable objects in detail. For sibling or cross-DOM
communication use Lightning Message Service, not a home-grown pubsub module:
import { LightningElement, wire } from 'lwc';
import { publish, subscribe, unsubscribe, MessageContext, APPLICATION_SCOPE } from 'lightning/messageService';
import RECORD_SELECTED from '@salesforce/messageChannel/Record_Selected__c';
export default class ListPanel extends LightningElement {
@wire(MessageContext) messageContext;
subscription;
connectedCallback() {
this.subscription = subscribe(this.messageContext, RECORD_SELECTED,
(message) => { this.selectedId = message.recordId; },
{ scope: APPLICATION_SCOPE });
}
disconnectedCallback() { unsubscribe(this.subscription); this.subscription = null; }
select(id) { publish(this.messageContext, RECORD_SELECTED, { recordId: id }); }
}7. Lifecycle hooks
| Hook | Fires | Safe to do | Never do |
constructor() | Once, before insertion | Call super() first, set non-DOM defaults | Touch this.template, read @api values |
connectedCallback() | On DOM insertion, parent to child | Subscribe (LMS, message bus), read @api props, kick off imperative calls | Assume children rendered |
render() | Before each render | Return an imported template for multi-template components | Side effects |
renderedCallback() | After every render, child to parent | One-time third-party init guarded by a boolean, imperative DOM reads | Mutate @api/wire-config props (infinite loop) |
disconnectedCallback() | On DOM removal | unsubscribe, clearInterval, abort fetches | Assume it runs on tab close |
errorCallback(error, stack) | Child throws in lifecycle/render | Render a fallback, log | Swallow silently |
8. Templates
<template>
<template lwc:if={hasRows}>
<ul>
<template for:each={rows} for:item="row" for:index="i">
<li key={row.id} data-id={row.id} onclick={handleSelect}>{row.name}</li>
</template>
</ul>
</template>
<template lwc:elseif={loading}><lightning-spinner alternative-text="Loading"></lightning-spinner></template>
<template lwc:else><p>No results.</p></template>
<ul>
<template iterator:it={rows}>
<li key={it.value.id} class={it.first}>{it.value.name}</li>
</template>
</ul>
<input lwc:ref="firstInput" type="text" />
<div lwc:dom="manual" class="chart"></div>
<slot name="footer"></slot>
</template>lwc:if/lwc:elseif/lwc:else supersede if:true/if:false; they cannot be combined on the same
element, and lwc:elseif/lwc:else must immediately follow their sibling. Expressions support only
simple dot notation - anything else goes in a getter. key is required on the first element inside
for:each/iterator: and must be a stable business id, never the index. Access refs with
this.refs.firstInput (do not use lwc:ref inside for:each).
9. Styling, shadow DOM, and light DOM
:host {
--slds-c-card-color-background: var(--slds-g-color-surface-1);
display: block;
}
:host(.compact) .row { padding: 0; }
lightning-input { --slds-c-input-color-border: var(--slds-g-color-border-accent-1); }Style with SLDS utility classes and SLDS styling hooks (CSS custom properties); do not reach across the shadow boundary with descendant selectors - it does not work and will not be supported. Opt into light DOM only when a third-party library or global stylesheet must see your markup:
export default class LightPanel extends LightningElement {
static renderMode = 'light'; // template must declare <template lwc:render-mode="light">
renderedCallback() { this.querySelector('.chart'); } // light DOM: no shadowRoot
}Light DOM gives up style scoping and event retargeting - state the reason in a code comment.
10. Navigation, toasts, labels, static resources
import { NavigationMixin } from 'lightning/navigation';
import { loadScript, loadStyle } from 'lightning/platformResourceLoader';
import CHARTJS from '@salesforce/resourceUrl/chartjs';
import LABEL_ESCALATE from '@salesforce/label/c.Escalate_Case';
import LANG from '@salesforce/i18n/lang';
export default class Panel extends NavigationMixin(LightningElement) {
label = { escalate: LABEL_ESCALATE };
lang = LANG;
chartInitialized = false;
goToRecord(recordId) {
this[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: { recordId, objectApiName: 'Case', actionName: 'view' }
});
}
async renderedCallback() {
if (this.chartInitialized) return;
this.chartInitialized = true;
await loadStyle(this, `${CHARTJS}/chart.css`);
await loadScript(this, `${CHARTJS}/chart.umd.js`);
this.renderChart();
}
}Never hardcode user-facing strings: import custom labels via @salesforce/label/c.Name and locale
data via @salesforce/i18n/*. Third-party JS must live in a static resource and load through
lightning/platformResourceLoader; CDN script tags are blocked by CSP.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
this.filters.status = 'x' on an untracked object and expecting a rerender | Field was not reassigned; no mutation detected | this.filters = { ...this.filters, status: 'x' } |
@track on every field | Deep proxying cost with no benefit; fields are already reactive | Remove; use @track only for in-place mutation |
Setting a wired config property inside renderedCallback() | Re-provision triggers rerender triggers hook: infinite loop | Set it in connectedCallback() or a handler |
@AuraEnabled(cacheable=true) on a method that performs DML | Runtime error; cacheable methods cannot write | Split into a cacheable read and a non-cacheable write |
Calling an imperative Apex writer and expecting @wire(getRecord) to update | LDS does not know the record changed | await notifyRecordUpdateAvailable([{recordId}]) |
new CustomEvent('select', { bubbles: true, composed: true }) for a parent handler | Leaks the event to the whole page; breaks encapsulation | Default (bubbles: false) and handle on the child tag |
Custom pubsub.js module for sibling communication | Not supported across DOM trees or Aura/Visualforce; leaks subscriptions | Lightning Message Service with MessageContext and unsubscribe |
element.shadowRoot.querySelector from a parent component | Breaks encapsulation; blocked/locked at runtime | @api method or event |
key={index} in for:each | Diffing reuses the wrong nodes on reorder | key={row.id} |
console.log left in shipped components | Leaks data to the browser console in production | Remove; vf-check lint flags no-console |
Building a form with 12 lightning-input fields plus manual DML | Reimplements field-level security, layouts, and validation | lightning-record-edit-form or lightning-record-form |
CDN <script src> or inline <script> | Blocked by CSP and Lightning Web Security | Static resource + loadScript |
Verification
# LWC unit tests and coverage (no org needed)
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" jest --changed
# Full local gate: prettier + eslint + Code Analyzer + jest
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" local --changed
# Live preview of the component against a scratch org (no deploy)
sf lightning dev component --name accountCard --target-org vf-dev
# Validate the bundle compiles server-side before deploying
sf project deploy start --source-dir force-app/main/default/lwc/accountCard \
--dry-run --target-org vf-dev
# Deploy, then verify in the org
sf project deploy start --source-dir force-app/main/default/lwc/accountCard --target-org vf-dev
sf org open --path lightning/n/Account_Console --target-org vf-devGates that apply to LWC work: requireJestForLwc (every bundle needs __tests__), jestCoverageMin
80, and analyzerFailSeverity 3. See skills sf-lwc-jest-testing, sf-code-analyzer-quality, and
sf-post-deploy-verification.
References
references/component-reference.md- decorators, lifecycle hooks, every template directive,js-meta.xmltargets,@salesforce/*module table.references/data-access-patterns.md- wire vs imperative vs LDS matrix, GraphQL wire adapter,reduceErrors, record-form components.references/events-and-messaging.md- CustomEvent propagation matrix, LMS setup, parent/child patterns, Aura/Visualforce interop.references/mcp-and-skills.md-lwc-experts,aura-expertsandexperts-validationtoolsets, per-tool GA status, the LWC skills that superseded them, and the checklist for accepting generated code.references/lwc-recipes-catalog.md- curated runnable patterns mapped totrailheadapps/lwc-recipes.- Official: Lightning Web Components Developer Guide, Lifecycle Hooks, renderedCallback(), XML Configuration File Elements, notifyRecordUpdateAvailable, Lightning Message Service, Lightning Web Security.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

