Migrating Visualforce pages and components to Lightning Web Components: controller-to-Apex-method translation, viewstate replacement, custom URL parameter handling, PageReference-to-NavigationMixin mapping, Lightning Out. NOT for new LWC development from scratch (use lwc-fundamentals) or Aura-to-LWC migration — use lwc/navigation-and-routing.
This skill activates when a practitioner needs to replace a Visualforce page with a Lightning Web Component, manage coexistence between VF and LWC during phased migration, or translate VF-specific patterns (viewstate, PageReference, renderAs) to LWC equivalents.
Before Starting
Gather this context before working on anything in this domain:
Audit every URL parameter the VF page consumes from ApexPages.currentPage().getParameters() — these become @api properties or CurrentPageReference query-param reads on the LWC side.
Confirm the page's render mode. renderAs="pdf", custom contentType, or email-template embedding are NOT replaceable by LWC — those VF pages must stay or move to Apex-driven document services.
List every action method (<apex:actionFunction>, <apex:commandButton>) and its return type. Action methods that return PageReference need the navigation translated; methods that mutate state via viewstate need to be re-architected as imperative Apex calls with explicit DTOs.
Core Concepts
1. Visualforce-to-LWC Capability Map
Visualforce Capability
LWC Equivalent
Migration Notes
<apex:page controller="Foo">
LWC @AuraEnabled Apex methods invoked via @wire or imperative call
No persistent server-side controller state; every call is stateless
Viewstate (form posts)
Client-side reactive state in JS + explicit Apex DML on submit
Eliminate hidden state; design DTOs for the Apex method signature
<apex:repeat> / <apex:pageBlockTable>
Template for:each or <lightning-datatable>
Use lightning-datatable for sortable / inline-edit tables
Site builder page; cookies/guest user constraints differ
Quick Action override
lightning__RecordAction + actionType ScreenAction in js-meta.xml
Record context auto-injected
Button URL / "View Source"
NOT a direct surface — requires Quick Action wrapping
Old VF buttons need a parallel migration path (see custom-button-to-action-migration)
3. Server Communication Translation
Every VF page communicated with its controller via the form post (<apex:form>) or AJAX (<apex:actionFunction>). LWC has two patterns and they are not equivalent:
Pattern
When to use
Cacheable?
Reactive on data change?
@wire(getData, { recordId: '$recordId' })
Read-heavy; want automatic refresh on dependency change
Yes (cacheable=true)
Yes — wire re-fires when input properties change
Imperative import getData from '@salesforce/apex/X.getData' then await getData({ recordId })
Read on user trigger or write operations
Optional
No — caller must re-invoke
LightningDataService via lightning/uiRecordApi
Standard CRUD on a single record without writing Apex
Yes
Yes — auto-refresh across components
VF developers tend to write a single getController() Apex method that returns a wrapper of everything the page needs. In LWC this is acceptable for one screen, but split if the screen has independently refreshing zones — each zone gets its own wire so refresh is granular.
4. Lightning Out as a Transitional Bridge
Lightning Out lets you embed an LWC into a remaining Visualforce page. This is the canonical mechanism for partial migration when the page surface itself cannot move yet (e.g., embedded in an external system iframe, or referenced by a hardcoded button URL).
The reverse — embedding VF inside LWC — is done via <iframe src="/apex/MyVfPage"> and is a code smell in production. Use it only as a strict transitional measure with a tracked removal date.
Common Patterns
Pattern 1: Read-Only VF Page → Wired LWC
When to use: A VF page renders read-only data computed by the controller (dashboards, account summaries, KPIs).
How it works:
Convert each controller get property into an @AuraEnabled(cacheable=true) static method returning a serializable DTO.
In the LWC, wire each method: @wire(getKpiSnapshot, { recordId: '$recordId' }) snapshot;.
Render via template binding ({snapshot.data.totalRevenue}).
Add <lightning-spinner> for the loading state and an error template branch for snapshot.error.
Replace the VF page with the LWC on the surface (App Builder page, custom tab).
Why not the alternative: Calling the controller imperatively defeats the wire's caching and refresh-on-input semantics. Wire is the right primitive for read-only views.
When to use: A VF page used <apex:inputField> in an <apex:form> to create or edit a record.
How it works:
Replace the VF page with <lightning-record-edit-form object-api-name="Account"> containing <lightning-input-field> for each field.
Handle onsuccess and onerror events instead of a custom save() controller method.
Use <lightning-record-form> (single-line) when the layout follows the page layout assignment exactly — eliminates field listing entirely.
Field-Level Security, validation rules, and field-level help text are honored automatically.
Why not the alternative: Writing a custom @AuraEnabled save method that calls update record re-implements features (FLS, validation rules, lookup search UI) that lightning-record-edit-form provides for free. Only build a custom Apex save when business logic spans multiple records or requires a transaction boundary the form can't express.
Pattern 3: PDF / Email VF Page Stays as Visualforce
When to use: The VF page uses renderAs="pdf", is the body of Messaging.SingleEmailMessage.setTemplateId(), or sets a custom contentType for download.
How it works:
Do NOT migrate. LWC has no equivalent for these capabilities.
If the LWC ecosystem needs to trigger the PDF, build an Apex @AuraEnabled method that calls Blob result = pageRef.getContentAsPDF() or getContent() and returns a Base64 string the LWC can save via the browser.
Document the retained VF page in the migration log with rationale "renderAs not portable."
Apply Visualforce Security Best Practices (CRUD/FLS checks, escaping) — these pages remain a security surface even when the rest of the org moves to LWC.
Why not the alternative: Re-implementing PDF generation in JavaScript (jsPDF, html2pdf) loses Salesforce's server-side rendering, breaks Locker/LWS compatibility, and inflates bundle size. Server-side getContentAsPDF() is the right primitive.
Pattern 4: Lightning Out Coexistence for Hardcoded VF URLs
When to use: Buttons, email links, or external systems link to a VF page URL that cannot be changed in the migration window.
How it works:
Build the LWC.
Create a thin VF page that uses $Lightning.use() and $Lightning.createComponent() to mount the LWC inside Lightning Out.
The original VF URL now serves the LWC inside a Lightning Out container.
Track the wrapper as transitional debt with a removal date when the upstream caller is updated to navigate directly.
Why not the alternative: Rewriting every external caller to a new URL is often blocked by external system release cycles. Lightning Out preserves the contract while modernizing the implementation.
Decision Guidance
Situation
Recommended Approach
Reason
VF page is read-only and lives on App Builder page
Direct rewrite to wired LWC
Wire pattern matches read-only nature; no viewstate to translate
VF page is a CRUD form on one object
Replace with lightning-record-form or lightning-record-edit-form
Page layout / FLS / validation handled natively
VF page uses renderAs="pdf"
Keep as VF; do not migrate
LWC has no PDF rendering capability
VF page is the body of an email template
Keep as VF; address as separate email migration
Email rendering surface is not LWC-eligible
VF page is invoked by a hardcoded URL from outside the org
Lightning Out wrapper VF page that mounts the new LWC
Preserves URL contract
VF page has heavy custom JavaScript with jQuery / Bootstrap
Audit JS first; many libs violate LWS — refactor before migrating
Lightning Web Security restrictions can block libs that worked in VF
VF page is a button override for a standard action
Re-architect to render via template binding (sanitized)
Bypassing escaping in VF is a known XSS surface; do not preserve
VF page sets viewstate via <apex:inputHidden> for tracking
Move tracking to a transient client-side property in the LWC
Viewstate has no LWC equivalent and is not needed
Recommended Workflow
Step-by-step instructions for an AI agent or practitioner working on this task:
Inventory the VF page surface. List the apex:page attributes (controller, extensions, standardController, renderAs, contentType, tabStyle), every apex: markup tag in use, every controller method (its return type and DML behavior), every URL parameter consumed, and every static resource referenced.
Decide migrate vs retain. Apply the Decision Guidance table. PDF, email-body, custom-content-type, and externally-linked URL pages are migration partial candidates; everything else is a full migration target.
Translate controller to @AuraEnabled methods. For each get property, expose a @AuraEnabled(cacheable=true) static method. For each action method, expose an @AuraEnabled(cacheable=false) method that returns explicit DTOs (no PageReference). Include with sharing and explicit FLS enforcement: WITH USER_MODE in SOQL, plus Security.stripInaccessible(AccessType.CREATABLE, records).getRecords() on write paths that assemble records from user input (AccessType.UPDATABLE when the DML is an update — the enum must match the operation). Do not carry WITH SECURITY_ENFORCED across from the old controller — it does not compile once the new class's .cls-meta.xmlapiVersion is 67.0+ (Summer '26 removed it).
Scaffold the LWC bundle. Create <componentName>.js, .html, .css, and .js-meta.xml. Set targets to match the original VF surface. Expose @api properties for any URL parameter the VF page received.
Wire data + handle navigation. Use @wire for read-only data, imperative for writes. Replace PageReference returns with this[NavigationMixin.Navigate]({ type, attributes }). Replace apex:commandButton action calls with JS event handlers that invoke the imperative method and then refreshApex(this.wiredHandle) on success.
Verify parity. Diff the rendered output against the VF page on identical data. Confirm FLS behavior (a user without field access must see the same hidden state). Test all URL parameter entry points.
Decommission the VF page. Once stable, remove the VF page from the App Builder / tab / button override. Delete the controller class only after confirming no other VF page still uses it. Keep a Lightning Out wrapper if external callers still hit the URL.
Review Checklist
Run through these before marking work in this area complete:
Every VF controller get property has a corresponding @AuraEnabled(cacheable=true) method
Every controller action method has been re-architected as an imperative @AuraEnabled method returning a serializable DTO (no PageReference)
All with sharing, CRUD, and FLS enforcement is explicit in the new Apex (WITH USER_MODE in SOQL, Security.stripInaccessible on write paths) — no WITH SECURITY_ENFORCED survived the port, which fails to compile at apiVersion 67.0+
LWC js-meta.xmltargets match every original VF surface (App Builder, Experience, Tab, etc.)
All PageReference redirects are translated to NavigationMixin.Navigate calls with the correct type and attributes
No <apex:outputText escape="false"> patterns survived (template binding sanitizes by default)
renderAs="pdf", contentType=..., and email-body VF pages are explicitly retained, not migrated
Lightning Out wrapper VF pages are documented with a removal date if any are deployed
Loaded JS libraries pass Lightning Web Security validation (run in LWS-enabled scratch org)
Static resources are loaded via loadScript / loadStyle, not via <apex:includeScript> references that no longer apply
Salesforce-Specific Gotchas
Non-obvious platform behaviors that cause real production problems:
Viewstate is not a feature you replace — it's a coupling you eliminate. Visualforce viewstate persists controller member variables across postbacks transparently. LWC has no server-side persistence between Apex method calls; every call is stateless. Code that depended on viewstate for tracking which row was edited, which step a wizard was on, or what the user just typed must be re-architected as explicit client-side state passed in DTOs. There is no "LWC viewstate flag" to flip.
apex:actionFunction JavaScript names don't exist anymore. Existing client JS that calls myActionFn() (auto-generated from <apex:actionFunction name="myActionFn">) breaks completely in LWC. There is no global JS namespace for component methods. Migration must rewrite every JS caller to import the Apex method directly and call it via async/await.
renderAs="pdf" and email-template VF pages cannot be migrated. These rely on the Visualforce server-side renderer (Apex getContentAsPDF, Messaging.SingleEmailMessage.setTemplateId). LWC has no equivalent. Attempting to "migrate" them leads to broken PDFs or unsendable emails. The correct outcome is to keep the VF page and document the retention.
Lightning Web Security blocks JS libraries that worked under Locker. Lightning Web Security (LWS) is the new client-side security architecture. Some third-party libraries that worked under Locker Service in Aura/VF break under LWS — particularly those that touch window directly, use eval, or manipulate cross-origin iframes. Migration must include an LWS compatibility test pass before declaring the LWC complete.
<apex:outputText escape="false"> patterns are a security trap. VF allowed bypassing HTML escaping with escape="false". Many existing pages used this for trivial reasons (rendering a <br> from a text area). Translating this verbatim to LWC by using lwc:dom="manual" or innerHTML recreates the XSS surface. The migration must sanitize inputs explicitly or re-render the data with safe primitives (<lightning-formatted-text> for line-break preservation).
URL parameter access changes from ApexPages.currentPage() to CurrentPageReference. VF reads URL parameters server-side via ApexPages.currentPage().getParameters().get('id'). LWC reads them client-side via @wire(CurrentPageReference) and accesses pageRef.state.c__id. The parameter name is also rewritten to add a c__ prefix when used in App Builder pages — a hardcoded URL like ?id=123 arriving at an LWC page becomes ?c__id=123. External callers must be updated.
Output Artifacts
Artifact
Description
LWC component bundle
.js, .html, .css, .js-meta.xml files replacing the Visualforce page
@AuraEnabled Apex class
Stateless service methods replacing the VF controller; with sharing + explicit FLS
Lightning Out wrapper VF page
Transitional shell that mounts the new LWC into the original VF URL
Migration audit log
Per-page record of every controller method, URL parameter, and renderAs use mapped to its LWC outcome (migrated / retained / refactored)
Updated button / tab / App Builder page
Surface configuration switched from VF to LWC reference
Related Skills
lwc/aura-to-lwc-migration — Use when the source is Aura, not Visualforce; many event-translation patterns overlap
lwc/lwc-imperative-apex — Use when porting apex:actionFunction patterns to LWC imperative calls
apex/apex-rest-and-aura-enabled — Use when designing the @AuraEnabled service layer that replaces the VF controller
admin/custom-button-to-action-migration — Use when the VF page was a button override; the button itself also needs migration
security/secure-coding-visualforce — Use when the retained VF pages need a security review before sign-off
1---2name: visualforce-to-lwc-migration3description: Migrating Visualforce pages and components to Lightning Web Components: controller-to-Apex-method translation, viewstate replacement, custom URL parameter handling, PageReference-to-NavigationMixin mapping, Lightning Out. NOT for new LWC development from scratch (use lwc-fundamentals) or Aura-to-LWC migration — use lwc/navigation-and-routing.4---56# Visualforce to LWC Migration78This skill activates when a practitioner needs to replace a Visualforce page with a Lightning Web Component, manage coexistence between VF and LWC during phased migration, or translate VF-specific patterns (viewstate, PageReference, renderAs) to LWC equivalents.910---1112## Before Starting1314Gather this context before working on anything in this domain:1516- Audit every URL parameter the VF page consumes from `ApexPages.currentPage().getParameters()` — these become `@api` properties or `CurrentPageReference` query-param reads on the LWC side.17- Confirm the page's render mode. `renderAs="pdf"`, custom `contentType`, or email-template embedding are NOT replaceable by LWC — those VF pages must stay or move to Apex-driven document services.18- List every action method (`<apex:actionFunction>`, `<apex:commandButton>`) and its return type. Action methods that return `PageReference` need the navigation translated; methods that mutate state via viewstate need to be re-architected as imperative Apex calls with explicit DTOs.1920---2122## Core Concepts2324### 1. Visualforce-to-LWC Capability Map2526| Visualforce Capability | LWC Equivalent | Migration Notes |27|---|---|---|28| `<apex:page controller="Foo">` | LWC `@AuraEnabled` Apex methods invoked via `@wire` or imperative call | No persistent server-side controller state; every call is stateless |29| Viewstate (form posts) | Client-side reactive state in JS + explicit Apex DML on submit | Eliminate hidden state; design DTOs for the Apex method signature |30| `<apex:repeat>` / `<apex:pageBlockTable>` | Template `for:each` or `<lightning-datatable>` | Use `lightning-datatable` for sortable / inline-edit tables |31| `<apex:inputField>` | `<lightning-input-field>` inside `<lightning-record-edit-form>` | Field-Level Security and validation rules apply automatically |32| `<apex:commandButton action="{!save}">` | Imperative Apex from JS, `try/catch`, refresh wire with `refreshApex` | No automatic page rerender — manage UI state explicitly |33| `<apex:actionFunction>` | Imperative Apex method import + JS call | No more "named JavaScript function that posts the form" |34| `PageReference` redirects | `NavigationMixin.Navigate({ type, attributes })` | URL-based; no controller round-trip |35| `<apex:outputText escape="false">` | Avoid; sanitize and use template binding | LWC sanitizes by default; bypassing is a security risk |36| `renderAs="pdf"` | NOT migratable — keep VF, or move to Apex `Blob.toPdf` / 3rd-party | Document rendering is not an LWC capability |37| `<apex:includeScript>` / `<apex:stylesheet>` | Static Resource via `loadScript` / `loadStyle` from `lightning/platformResourceLoader` | Loaded async; respect Locker/Lightning Web Security |3839### 2. Where the VF Page Lives Determines the Target Surface4041LWC `js-meta.xml` `targets` must match the surface the VF page was used on. Wrong target metadata = component does not appear.4243| VF Embedding Surface | Required LWC `target` | Notes |44|---|---|---|45| Custom Tab | `lightning__Tab` | Same UI position as VF custom tab |46| Object record page | `lightning__RecordPage` | Recordpage replacement; expose `@api recordId` |47| App home page | `lightning__HomePage` | App Builder page only |48| Experience Cloud page | `lightningCommunity__Page` + `lightningCommunity__Default` | Site builder page; cookies/guest user constraints differ |49| Quick Action override | `lightning__RecordAction` + actionType `ScreenAction` in `js-meta.xml` | Record context auto-injected |50| Button URL / "View Source" | NOT a direct surface — requires Quick Action wrapping | Old VF buttons need a parallel migration path (see custom-button-to-action-migration) |5152### 3. Server Communication Translation5354Every VF page communicated with its controller via the form post (`<apex:form>`) or AJAX (`<apex:actionFunction>`). LWC has two patterns and they are not equivalent:5556| Pattern | When to use | Cacheable? | Reactive on data change? |57|---|---|---|---|58| `@wire(getData, { recordId: '$recordId' })` | Read-heavy; want automatic refresh on dependency change | Yes (`cacheable=true`) | Yes — wire re-fires when input properties change |59| Imperative `import getData from '@salesforce/apex/X.getData'` then `await getData({ recordId })` | Read on user trigger or write operations | Optional | No — caller must re-invoke |60| `LightningDataService` via `lightning/uiRecordApi` | Standard CRUD on a single record without writing Apex | Yes | Yes — auto-refresh across components |6162VF developers tend to write a single `getController()` Apex method that returns a wrapper of everything the page needs. In LWC this is acceptable for one screen, but split if the screen has independently refreshing zones — each zone gets its own wire so refresh is granular.6364### 4. Lightning Out as a Transitional Bridge6566Lightning Out lets you embed an LWC into a remaining Visualforce page. This is the canonical mechanism for partial migration when the page surface itself cannot move yet (e.g., embedded in an external system iframe, or referenced by a hardcoded button URL).6768The reverse — embedding VF inside LWC — is done via `<iframe src="/apex/MyVfPage">` and is a code smell in production. Use it only as a strict transitional measure with a tracked removal date.6970---7172## Common Patterns7374### Pattern 1: Read-Only VF Page → Wired LWC7576**When to use:** A VF page renders read-only data computed by the controller (dashboards, account summaries, KPIs).7778**How it works:**791. Convert each controller `get` property into an `@AuraEnabled(cacheable=true)` static method returning a serializable DTO.802. In the LWC, wire each method: `@wire(getKpiSnapshot, { recordId: '$recordId' }) snapshot;`.813. Render via template binding (`{snapshot.data.totalRevenue}`).824. Add `<lightning-spinner>` for the loading state and an error template branch for `snapshot.error`.835. Replace the VF page with the LWC on the surface (App Builder page, custom tab).8485**Why not the alternative:** Calling the controller imperatively defeats the wire's caching and refresh-on-input semantics. Wire is the right primitive for read-only views.8687### Pattern 2: Form-Posting VF Page → LightningRecordEditForm8889**When to use:** A VF page used `<apex:inputField>` in an `<apex:form>` to create or edit a record.9091**How it works:**921. Replace the VF page with `<lightning-record-edit-form object-api-name="Account">` containing `<lightning-input-field>` for each field.932. Handle `onsuccess` and `onerror` events instead of a custom `save()` controller method.943. Use `<lightning-record-form>` (single-line) when the layout follows the page layout assignment exactly — eliminates field listing entirely.954. Field-Level Security, validation rules, and field-level help text are honored automatically.9697**Why not the alternative:** Writing a custom `@AuraEnabled` save method that calls `update record` re-implements features (FLS, validation rules, lookup search UI) that `lightning-record-edit-form` provides for free. Only build a custom Apex save when business logic spans multiple records or requires a transaction boundary the form can't express.9899### Pattern 3: PDF / Email VF Page Stays as Visualforce100101**When to use:** The VF page uses `renderAs="pdf"`, is the body of `Messaging.SingleEmailMessage.setTemplateId()`, or sets a custom `contentType` for download.102103**How it works:**1041. Do NOT migrate. LWC has no equivalent for these capabilities.1052. If the LWC ecosystem needs to trigger the PDF, build an Apex `@AuraEnabled` method that calls `Blob result = pageRef.getContentAsPDF()` or `getContent()` and returns a Base64 string the LWC can save via the browser.1063. Document the retained VF page in the migration log with rationale "renderAs not portable."1074. Apply Visualforce Security Best Practices (CRUD/FLS checks, escaping) — these pages remain a security surface even when the rest of the org moves to LWC.108109**Why not the alternative:** Re-implementing PDF generation in JavaScript (jsPDF, html2pdf) loses Salesforce's server-side rendering, breaks Locker/LWS compatibility, and inflates bundle size. Server-side `getContentAsPDF()` is the right primitive.110111### Pattern 4: Lightning Out Coexistence for Hardcoded VF URLs112113**When to use:** Buttons, email links, or external systems link to a VF page URL that cannot be changed in the migration window.114115**How it works:**1161. Build the LWC.1172. Create a thin VF page that uses `$Lightning.use()` and `$Lightning.createComponent()` to mount the LWC inside Lightning Out.1183. The original VF URL now serves the LWC inside a Lightning Out container.1194. Track the wrapper as transitional debt with a removal date when the upstream caller is updated to navigate directly.120121**Why not the alternative:** Rewriting every external caller to a new URL is often blocked by external system release cycles. Lightning Out preserves the contract while modernizing the implementation.122123---124125## Decision Guidance126127| Situation | Recommended Approach | Reason |128|---|---|---|129| VF page is read-only and lives on App Builder page | Direct rewrite to wired LWC | Wire pattern matches read-only nature; no viewstate to translate |130| VF page is a CRUD form on one object | Replace with `lightning-record-form` or `lightning-record-edit-form` | Page layout / FLS / validation handled natively |131| VF page uses `renderAs="pdf"` | Keep as VF; do not migrate | LWC has no PDF rendering capability |132| VF page is the body of an email template | Keep as VF; address as separate email migration | Email rendering surface is not LWC-eligible |133| VF page is invoked by a hardcoded URL from outside the org | Lightning Out wrapper VF page that mounts the new LWC | Preserves URL contract |134| VF page has heavy custom JavaScript with jQuery / Bootstrap | Audit JS first; many libs violate LWS — refactor before migrating | Lightning Web Security restrictions can block libs that worked in VF |135| VF page is a button override for a standard action | Replace with Quick Action launching the LWC | See `admin/custom-button-to-action-migration` |136| VF page uses inline `<apex:outputText escape="false">` | Re-architect to render via template binding (sanitized) | Bypassing escaping in VF is a known XSS surface; do not preserve |137| VF page sets viewstate via `<apex:inputHidden>` for tracking | Move tracking to a transient client-side property in the LWC | Viewstate has no LWC equivalent and is not needed |138139---140141## Recommended Workflow142143Step-by-step instructions for an AI agent or practitioner working on this task:1441451. **Inventory the VF page surface.** List the `apex:page` attributes (`controller`, `extensions`, `standardController`, `renderAs`, `contentType`, `tabStyle`), every `apex:` markup tag in use, every controller method (its return type and DML behavior), every URL parameter consumed, and every static resource referenced.1462. **Decide migrate vs retain.** Apply the Decision Guidance table. PDF, email-body, custom-content-type, and externally-linked URL pages are migration *partial* candidates; everything else is a full migration target.1473. **Translate controller to `@AuraEnabled` methods.** For each `get` property, expose a `@AuraEnabled(cacheable=true)` static method. For each action method, expose an `@AuraEnabled(cacheable=false)` method that returns explicit DTOs (no `PageReference`). Include `with sharing` and explicit FLS enforcement: `WITH USER_MODE` in SOQL, plus `Security.stripInaccessible(AccessType.CREATABLE, records).getRecords()` on write paths that assemble records from user input (`AccessType.UPDATABLE` when the DML is an update — the enum must match the operation). Do not carry `WITH SECURITY_ENFORCED` across from the old controller — it does not compile once the new class's `.cls-meta.xml` `apiVersion` is 67.0+ (Summer '26 removed it).1484. **Scaffold the LWC bundle.** Create `<componentName>.js`, `.html`, `.css`, and `.js-meta.xml`. Set `targets` to match the original VF surface. Expose `@api` properties for any URL parameter the VF page received.1495. **Wire data + handle navigation.** Use `@wire` for read-only data, imperative for writes. Replace `PageReference` returns with `this[NavigationMixin.Navigate]({ type, attributes })`. Replace `apex:commandButton` action calls with JS event handlers that invoke the imperative method and then `refreshApex(this.wiredHandle)` on success.1506. **Verify parity.** Diff the rendered output against the VF page on identical data. Confirm FLS behavior (a user without field access must see the same hidden state). Test all URL parameter entry points.1517. **Decommission the VF page.** Once stable, remove the VF page from the App Builder / tab / button override. Delete the controller class only after confirming no other VF page still uses it. Keep a Lightning Out wrapper if external callers still hit the URL.152153---154155## Review Checklist156157Run through these before marking work in this area complete:158159- [ ] Every VF controller `get` property has a corresponding `@AuraEnabled(cacheable=true)` method160- [ ] Every controller action method has been re-architected as an imperative `@AuraEnabled` method returning a serializable DTO (no `PageReference`)161- [ ] All `with sharing`, CRUD, and FLS enforcement is explicit in the new Apex (`WITH USER_MODE` in SOQL, `Security.stripInaccessible` on write paths) — no `WITH SECURITY_ENFORCED` survived the port, which fails to compile at `apiVersion` 67.0+162- [ ] LWC `js-meta.xml` `targets` match every original VF surface (App Builder, Experience, Tab, etc.)163- [ ] All `PageReference` redirects are translated to `NavigationMixin.Navigate` calls with the correct `type` and `attributes`164- [ ] No `<apex:outputText escape="false">` patterns survived (template binding sanitizes by default)165- [ ] `renderAs="pdf"`, `contentType=...`, and email-body VF pages are explicitly retained, not migrated166- [ ] Lightning Out wrapper VF pages are documented with a removal date if any are deployed167- [ ] Loaded JS libraries pass Lightning Web Security validation (run in LWS-enabled scratch org)168- [ ] Static resources are loaded via `loadScript` / `loadStyle`, not via `<apex:includeScript>` references that no longer apply169170---171172## Salesforce-Specific Gotchas173174Non-obvious platform behaviors that cause real production problems:1751761. **Viewstate is not a feature you replace — it's a coupling you eliminate.** Visualforce viewstate persists controller member variables across postbacks transparently. LWC has no server-side persistence between Apex method calls; every call is stateless. Code that depended on viewstate for tracking which row was edited, which step a wizard was on, or what the user just typed must be re-architected as explicit client-side state passed in DTOs. There is no "LWC viewstate flag" to flip.1771782. **`apex:actionFunction` JavaScript names don't exist anymore.** Existing client JS that calls `myActionFn()` (auto-generated from `<apex:actionFunction name="myActionFn">`) breaks completely in LWC. There is no global JS namespace for component methods. Migration must rewrite every JS caller to import the Apex method directly and call it via async/await.1791803. **`renderAs="pdf"` and email-template VF pages cannot be migrated.** These rely on the Visualforce server-side renderer (Apex `getContentAsPDF`, `Messaging.SingleEmailMessage.setTemplateId`). LWC has no equivalent. Attempting to "migrate" them leads to broken PDFs or unsendable emails. The correct outcome is to keep the VF page and document the retention.1811824. **Lightning Web Security blocks JS libraries that worked under Locker.** Lightning Web Security (LWS) is the new client-side security architecture. Some third-party libraries that worked under Locker Service in Aura/VF break under LWS — particularly those that touch `window` directly, use `eval`, or manipulate cross-origin iframes. Migration must include an LWS compatibility test pass before declaring the LWC complete.1831845. **`<apex:outputText escape="false">` patterns are a security trap.** VF allowed bypassing HTML escaping with `escape="false"`. Many existing pages used this for trivial reasons (rendering a `<br>` from a text area). Translating this verbatim to LWC by using `lwc:dom="manual"` or `innerHTML` recreates the XSS surface. The migration must sanitize inputs explicitly or re-render the data with safe primitives (`<lightning-formatted-text>` for line-break preservation).1851866. **URL parameter access changes from `ApexPages.currentPage()` to `CurrentPageReference`.** VF reads URL parameters server-side via `ApexPages.currentPage().getParameters().get('id')`. LWC reads them client-side via `@wire(CurrentPageReference)` and accesses `pageRef.state.c__id`. The parameter name is also rewritten to add a `c__` prefix when used in App Builder pages — a hardcoded URL like `?id=123` arriving at an LWC page becomes `?c__id=123`. External callers must be updated.187188---189190## Output Artifacts191192| Artifact | Description |193|---|---|194| LWC component bundle | `.js`, `.html`, `.css`, `.js-meta.xml` files replacing the Visualforce page |195| `@AuraEnabled` Apex class | Stateless service methods replacing the VF controller; `with sharing` + explicit FLS |196| Lightning Out wrapper VF page | Transitional shell that mounts the new LWC into the original VF URL |197| Migration audit log | Per-page record of every controller method, URL parameter, and renderAs use mapped to its LWC outcome (migrated / retained / refactored) |198| Updated button / tab / App Builder page | Surface configuration switched from VF to LWC reference |199200---201202## Related Skills203204- `lwc/aura-to-lwc-migration` — Use when the source is Aura, not Visualforce; many event-translation patterns overlap205- `lwc/lwc-imperative-apex` — Use when porting `apex:actionFunction` patterns to LWC imperative calls206- `apex/apex-rest-and-aura-enabled` — Use when designing the `@AuraEnabled` service layer that replaces the VF controller207- `admin/custom-button-to-action-migration` — Use when the VF page was a button override; the button itself also needs migration208- `security/secure-coding-visualforce` — Use when the retained VF pages need a security review before sign-off
Run npx skillmds add pranavnagrecha/visualforce-to-lwc-migration in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Migrating Visualforce pages and components to Lightning Web Components: controller-to-Apex-method translation, viewstate replacement, custom URL parameter handling, PageReference-to-NavigationMixin mapping, Lightning Out. NOT for new LWC development from scratch (use lwc-fundamentals) or Aura-to-LWC migration — use lwc/navigation-and-routing. It is listed under Coding & Dev Tools on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: executes scripts. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
PranavNagrecha (@pranavnagrecha) published this skill. Their other Agent Skills are listed on their SkillMD profile.