ESC

AI-powered search across all blog posts and tools

LWC Β· January 6, 2026

LWC Wire Service - The Complete Guide

The wire service is LWC's declarative data fetching mechanism. Understanding its caching behavior, reactivity model, and when to use it over imperative Apex calls is essential for building fast, correct components.

☕ 10 min read 📅 January 6, 2026
  • The wire service provisions data reactively β€” when its parameters change, it automatically re-fetches, eliminating lifecycle management boilerplate
  • Wire results are cached by the LDS cache and shared across components on the same page β€” this is a performance feature, not a bug
  • Use imperative Apex when you need explicit control over timing, need to call on user interaction, or need to handle mutually exclusive loading states

The wire service is one of those LWC features that looks simple from the outside β€” you decorate a property or function with @wire and data appears β€” but has enough depth underneath that I regularly see developers run into unexpected behavior because they treated it as a black box. This guide covers everything from the fundamentals to the edge cases that matter in production.

What the Wire Service Actually Does

At its core, the wire service is a reactive data provisioning mechanism. It connects a wire adapter (a data source) to a component property or function. When the inputs to the wire adapter change, the wire service automatically re-invokes the adapter and delivers new data to your component.

The key word is reactive. You do not call the wire service β€” you declare a binding, and the runtime manages when and how data flows.

Wire Service β€” Data Flow Architecture
Wire Service β€” Data Flow ArchitectureLWC Component@api recordId (reactive)Changes trigger re-wire@wire(getRecord, { recordId,fields } account;account.data β€” record dataaccount.error β€” error infoBoth may be undefinedwhile loadingWire ServiceParameter TrackingDetects reactive changesLDS Cache CheckCache hit = instant dataResult DeliveryEmits { data, error }Shared across componentsServer / CacheLDS (Lightning Data Service)Client-side record cacheSalesforce PlatformRecord API / ApexAuto-refresh on DMLLDS tracks field versionsparam changerequestresponsedata/errorLDS cache is shared β€” all components wiring to the same record share one server request

Wire Adapters

getRecord

The most commonly used wire adapter retrieves a single record by ID:

import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import BILLING_CITY_FIELD from '@salesforce/schema/Account.BillingCity';

const FIELDS = [NAME_FIELD, INDUSTRY_FIELD, BILLING_CITY_FIELD];

export default class AccountSummary extends LightningElement {
    @api recordId;

    @wire(getRecord, { recordId: '$recordId', fields: FIELDS })
    account;

    get name() {
        return getFieldValue(this.account.data, NAME_FIELD);
    }

    get industry() {
        return getFieldValue(this.account.data, INDUSTRY_FIELD);
    }

    get isLoading() {
        return !this.account.data && !this.account.error;
    }
}
πŸ’‘ Pro Tip

The $recordId prefix on the parameter is what makes it reactive β€” when the recordId property changes, the wire service automatically re-fetches. Always use imported schema references (@salesforce/schema/Object.Field) rather than string literals for field names. Schema references are validated at compile time and trigger re-wire when they change.

getRecordUi

getRecordUi returns not just record data but also the object metadata, layout information, and picklist values in a single call:

import { getRecordUi } from 'lightning/uiRecordApi';

@wire(getRecordUi, {
    recordIds: '$recordId',
    layoutTypes: ['Full'],
    modes: ['View']
})
recordUi;

get fieldLabel() {
    if (!this.recordUi.data) return '';
    const objectInfo = this.recordUi.data.objectInfos.Account;
    return objectInfo.fields.Industry.label;
}
ℹ️ Info

getRecordUi is heavier than getRecord β€” only use it when you genuinely need layout or metadata information.

Custom Apex Wire Adapters

For complex queries that go beyond what the platform wire adapters support, you can wire to an Apex method:

JavaScript Component

import getRelatedOpportunities from '@salesforce/apex/AccountController.getRelatedOpportunities';

@wire(getRelatedOpportunities, { accountId: '$recordId', stage: '$selectedStage' })
opportunities;

Apex Controller

public class AccountController {

    @AuraEnabled(cacheable=true)
    public static List<Opportunity> getRelatedOpportunities(
        Id accountId,
        String stage
    ) {
        return [
            SELECT Id, Name, Amount, StageName, CloseDate
            FROM Opportunity
            WHERE AccountId = :accountId
            AND StageName = :stage
            ORDER BY CloseDate ASC
            LIMIT 100
        ];
    }
}
🚨 Important

The cacheable=true annotation is required for wire service usage. This tells the platform that the method’s results can be safely cached and shared. Methods with cacheable=true cannot perform DML β€” they are read-only.

Reactive Properties

The $ prefix on a wire parameter creates a reactive binding. When that property changes, the wire service re-runs the adapter automatically.

export default class OpportunityFilter extends LightningElement {
    @api recordId;
    selectedStage = 'Prospecting'; // changes trigger re-wire

    @wire(getOpportunitiesByStage, {
        accountId: '$recordId',
        stage: '$selectedStage'
    })
    opportunities;

    handleStageChange(event) {
        this.selectedStage = event.detail.value;
        // No manual re-fetch needed β€” wire re-runs automatically
    }
}
Using getter-based reactive parameters

You can also make reactive parameters from computed values using getter-based objects:

get opportunityFilters() {
    return {
        accountId: this.recordId,
        stage: this.selectedStage,
        minAmount: this.minAmount
    };
}

@wire(getFilteredOpportunities, '$opportunityFilters')
opportunities;

Error Handling

A wire result always arrives as an object with data and error properties. At any given moment exactly one of the following states is true:

  1. Both data and error are undefined β€” loading state
  2. data is populated, error is undefined β€” success
  3. data is undefined, error is populated β€” error state

HTML Template

<template if:true={isLoading}>
    <lightning-spinner alternative-text="Loading..."></lightning-spinner>
</template>

<template if:true={account.error}>
    <div class="error-message">
        {errorMessage}
    </div>
</template>

<template if:true={account.data}>
    <!-- render data -->
</template>

JavaScript Controller

get isLoading() {
    return !this.account.data && !this.account.error;
}

get errorMessage() {
    if (!this.account.error) return '';
    return this.account.error.body?.message || 'An error occurred';
}
⚠️ Warning

Do not assume that a missing error means success β€” during the loading phase, both data and error are undefined.

Caching Behavior and refreshApex

How LDS Caching Works

The Lightning Data Service maintains a client-side cache keyed by record ID and field list. When two components on the same page wire to the same record with the same fields, only one server request is made. The second component receives data from the cache.

This is a significant performance benefit for record-heavy pages. It also means that if a DML operation on the server modifies a record, LDS detects the version change and automatically notifies all wired components β€” they re-render with fresh data without you writing any refresh logic.

When to Use refreshApex

LDS cache auto-refresh handles most scenarios. You need refreshApex specifically when:

  1. You make an imperative DML call from the component and need to immediately reflect changes
  2. Your wire adapter is a custom Apex method (not an LDS adapter) that returns stale data after an operation
import { refreshApex } from '@salesforce/apex';

// Store the wired result object for later refresh
wiredOpportunitiesResult; // store reference

@wire(getRelatedOpportunities, { accountId: '$recordId' })
wiredOpportunities(result) {
    this.wiredOpportunitiesResult = result; // store for refreshApex
    if (result.data) {
        this.opportunities = result.data;
    } else if (result.error) {
        this.error = result.error;
    }
}

async handleSaveOpportunity(event) {
    try {
        await saveOpportunity({ opportunity: this.editedOpp });
        await refreshApex(this.wiredOpportunitiesResult); // force re-fetch
    } catch (error) {
        this.error = error;
    }
}
ℹ️ Info

Note the function-style wire handler in the example above β€” this pattern stores the full provisioned result object for refreshApex. However, refreshApex also works with property-style wire declarations: if you use @wire(...) opportunities;, you can call refreshApex(this.opportunities) by passing the wired property directly. The function-style approach simply gives you more control over separating data and error handling.

Wire vs Imperative Apex

This is the most common decision point for LWC developers.

Wire vs Imperative Apex β€” When to Use Which
Wire vs Imperative Apex β€” When to Use WhichUse @wire when…Data should load as soon as component rendersParameters change and data should auto-refreshMultiple components need the same data (shared cache)The operation is read-only (cacheable=true OK)Using standard LDS adapters (getRecord, etc.)You want automatic re-render on server-side changesData is tied to the record context (page-level data)Key trait: declarative, lifecycle-managed, cachedUse imperative Apex when…Data loads on a user action (button click, search)The method performs DML (insert/update)You need mutually exclusive loading statesYou need to handle errors in a custom wayThe call should not happen on initial loadYou need to await the result before proceedingThe Apex method is not cacheable (stateful)Key trait: explicit, async/await, full control

A practical example contrasting both:

Wire β€” Auto-loads

// Loads on component init, refreshes when recordId changes
@wire(getAccountContacts, { accountId: '$recordId' })
contacts;

Imperative β€” On user action

// Loads on search button click with user-entered query
async handleSearch() {
    this.isLoading = true;
    try {
        this.searchResults = await searchContacts({
            accountId: this.recordId,
            searchTerm: this.searchTerm
        });
    } catch (error) {
        this.error = error;
    } finally {
        this.isLoading = false;
    }
}
πŸ’‘ Pro Tip

You can and should use both patterns in the same component β€” wire for the initial record context data, imperative for user-initiated operations.

Custom Wire Adapters

Beyond wiring to Apex, you can build custom wire adapters using the @salesforce/wire/service APIs. This is advanced territory but useful for building reusable data provisioning components:

Custom Wire Adapter Implementation
import { register, ValueChangedEvent } from '@lwc/wire-service';

const wireService = Symbol('customWireAdapter');

function CustomWireAdapter(dataCallback) {
    this.dataCallback = dataCallback;
}

CustomWireAdapter.prototype.connect = function() {
    // Called when component is connected to DOM
    this.dataCallback({ data: null, error: null }); // initial loading state
    this.fetchData();
};

CustomWireAdapter.prototype.update = function(config) {
    // Called when config parameters change
    this.config = config;
    this.fetchData();
};

CustomWireAdapter.prototype.disconnect = function() {
    // Called when component is removed β€” clean up subscriptions
};

CustomWireAdapter.prototype.fetchData = async function() {
    try {
        const data = await fetchFromSomeSource(this.config);
        this.dataCallback({ data, error: undefined });
    } catch (error) {
        this.dataCallback({ data: undefined, error });
    }
};

register(wireService, CustomWireAdapter);
export { wireService };

Custom wire adapters are most useful for ISV packages that expose data provisioning APIs or for centralizing access to external APIs that multiple components need.

The Problem

Scenario: A component wires to a custom Apex method that returns a list of related Opportunities. After the user saves a new Opportunity via an imperative callout, the wired list does not refresh β€” it still shows the old data.

The Solution

Call refreshApex after the imperative save. With property-style wire (@wire(…) opportunities;), pass the wired property directly: await refreshApex(this.opportunities). Alternatively, use a function-style handler to store the provisioned result object: @wire(getRelatedOpportunities, { accountId: '$recordId' }) wiredOpportunities(result) { this.wiredResult = result; ... }, then call await refreshApex(this.wiredResult). Both approaches work β€” the function-style handler gives you more control over separating data and error handling.

πŸ’‘ Pro Tip

When a parent component needs to share record data with multiple child components, wire to the record once in the parent and pass specific field values down as @api properties. This consolidates LDS requests to a single wire call rather than each child independently wiring to the same record β€” reducing both server calls and component complexity.

Performance Patterns

Batch your field requests: If five components on a page all wire to the same Account record but request different fields, LDS will make separate requests. Structure your components so a parent component wires to the record with all needed fields and passes data down to children as @api properties.

Avoid wiring in loops: If you have a list of records and each list item component wires independently to its record, you generate N wire calls. Prefer fetching all records in the parent and passing down data.

Use getRecordNotifyChange: After imperative DML that modifies a record, call getRecordNotifyChange([{ recordId }]) to notify LDS that the record is stale:

import { getRecordNotifyChange } from 'lightning/uiRecordApi';

async handleSave() {
    await updateRecord({ fields: { Id: this.recordId, ...this.changes } });
    getRecordNotifyChange([{ recordId: this.recordId }]);
}

What patterns have you found most useful for managing complex wire service data flows in large LWC component trees?


Test Your Knowledge

Why must Apex methods used with @wire have cacheable=true?
When should you use refreshApex instead of relying on LDS auto-refresh?

How did this article make you feel?

Comments

Salesforce Tip

🎉

You finished this article!

What to read next

Contents