Tuesday, December 20, 2022

LWC Client-Side Caching of Apex Method Results

Client-Side Caching of Apex Method Results

To improve runtime performance, annotate the Apex method with @AuraEnabled(cacheable=true), which caches the method results on the client. To set cacheable=true, a method must only get data, it can’t mutate (change) data.

Marking a method as cacheable improves your component’s performance by quickly showing cached data from client-side storage without waiting for a server trip. If the cached data is stale, the framework retrieves the latest data from the server. Caching is especially beneficial for users on high latency, slow, or unreliable connections. Cacheable methods perform better, so use them when possible.

To use @wire to call an Apex method, you must set cacheable=true.

To call an Apex method imperatively, you can choose to set cacheable=true.

The caching refresh time is the duration in seconds before an entry is refreshed in storage. The refresh time is automatically configured in Lightning Experience and the Salesforce mobile app.

The default cache duration can change as we optimize the platform. When you design your Lightning web components, don't assume a cache duration. If a component knows that a cached value is invalid, for example when underlying data changes, it can query the server for updated data and refresh the cache with refreshApex() from @salesforce/apex.

Refresh the Cache When Calling a Method Imperatively

To refresh stale Apex data, invoke the Apex method and then call getRecordNotifyChange(recordIds) to update the Lightning Data Service (LDS) cache. Lightning Data Service doesn’t manage data provisioned by imperative Apex calls. After you invoke the Apex method, call getRecordNotifyChange(recordIds) to signal to Lightning Data Service that some records are stale and refresh those records in the cache.

import { LightningElement, wire } from 'lwc';
import { getRecord, getRecordNotifyChange } from 'lightning/uiRecordApi';
import apexUpdateRecord from '@salesforce/apex/Controller.apexUpdateRecord';
 
export default class Example extends LightningElement {
    @api recordId;
 
    // Wire a record.
    @wire(getRecord, { recordId: '$recordId', fields: ... })
    record;
 
    async handler() {
      // Update the record via Apex.
      await apexUpdateRecord(this.recordId);
      // Notify LDS that you've changed the record outside its mechanisms.
      getRecordNotifyChange([{recordId: this.recordId}]);
    }
    }

Refresh the Cache When Using @wire

To refresh Apex data provisioned via an Apex @wire, call refreshApex(). The function provisions the data using the configuration bound to the @wire and updates the cache.

Note The parameter you refresh with refreshApex() must be an object that was previously emitted by an Apex @wire.

Sometimes, you know that the cache is stale. If the cache is stale, the component needs fresh data. To query the server for updated data and refresh the cache, import and call the refreshApex() function. Invoke refreshApex() only when necessary because it incurs a network trip to query the server.

The refreshApex() function returns a Promise. When the Promise is resolved, the data in the wire is fresh. The return value from the Apex method is only available on the @wire. The @wire has fresh data when the Promise resolves, but the actual value to which the Promise resolves is meaningless. Use a then() block to work with the refreshed data, such as setting a property on the page.

import { refreshApex } from '@salesforce/apex';
refreshApex(valueProvisionedByApexWireService)
  • Where valueProvisionedByApexWireService is either a property annotated with an Apex @wire, or if you annotated a function, the argument that the wired function receives.
    // Example of refreshing data for a wired property
    // after you update data via an LDS-managed module (lightning/uiRecordApi).
    
    import { updateRecord } from 'lightning/uiRecordApi';
    import { refreshApex } from '@salesforce/apex';
    import getOpptyOverAmount from '@salesforce/apex/OpptyController.getOpptyOverAmount;
    
    @wire(getOpptyOverAmount, { amount: '$amount' })
    opptiesOverAmount;
    
    // Update the record using updateRecord(recordInput) 
    // Refresh Apex data that the wire service provisioned
    handler() { 
      updateRecord(recordInput).then(() => {
        refreshApex(this.opptiesOverAmount);
      });
    }
    // Example of refreshing data for a wired property
    // after you update data via imperative Apex.
    
    import { refreshApex } from '@salesforce/apex';
    import { getRecordNotifyChange } from 'lightning/uiRecordApi';
    import getOpptyOverAmount from '@salesforce/apex/OpptyController.getOpptyOverAmount;
    
    @wire(getOpptyOverAmount, { amount: '$amount' })
    opptiesOverAmount;
    
    // Update the record in Apex, such as via a button click
    // Refresh Apex data that the wire service provisioned
    handler() { 
      updateRecordApexMethod()
      .then(() => {
          refreshApex(this.opptiesOverAmount);
          getRecordNotifyChange(recordIds); // Refresh the Lightning Data Service cache
      });
    }
    //Example of refreshing data for a wired function
    
    import { refreshApex } from '@salesforce/apex';
    import getActivityHistory from '@salesforce/apex/ActivityController.getActivityHistory;
    
    
    @wire(getActivityHistory, { accountId: '$recordId', max: '500' })
    wiredGetActivityHistory(value) {
        // Hold on to the provisioned value so we can refresh it later.
        this.wiredActivities = value;
        // Destructure the provisioned value 
        const { data, error } = value;
        if (data) { ... }
        else if (error) { ... }
        ...
    }
    handler() {
      refreshApex(this.wiredActivities);
    }

Example: Refresh the Cache for a Wired Property

Let’s look at an example. This component displays a list of opportunities over a specified amount. A user can click to mark all the opportunities as Won. When the opportunities are updated, the cache data becomes stale. So after the code updates the opportunities, it calls refreshApex() to query the server for updated data and refresh the cache.

Shows a list of opportunities over a specified amount.
<!-- opportunitiesOverAmount.html -->
<template>
    <!-- Display a list of opportunities -->
    <p>List of opportunities over
        <lightning-formatted-number value={amount} format-style="currency"></lightning-formatted-number>
    </p>
    <template if:true={opptiesOverAmount.data}>
        <template for:each={opptiesOverAmount.data} for:item="oppty" for:index="idx">
            <div key={oppty.Id} class="slds-m-bottom_medium">
                <p>{idx}. {oppty.Name}</p>
                <p><lightning-formatted-number value={oppty.Amount} format-style="currency"></lightning-formatted-number></p>
                <p><lightning-formatted-date-time value={oppty.CloseDate}></lightning-formatted-date-time></p>
                <p><lightning-badge label={oppty.StageName}></lightning-badge></p>
            </div>
        </template>
        <!-- Click the button to change the opportunities. Requires the data to be refetched
             and rerendered -->
        <lightning-button label="Mark all as Closed Won" onclick={handleClick}></lightning-button>
    </template>
</template>

Let’s look at the JavaScript code for the <c-opportunities-over-amount> component. First it imports refreshApex and the Apex methods from @salesforce/apex.

Then it calls the Apex method getOpptyOverAmount with a dynamic value, amount. The data is rerequested whenever this.amount changes. The system provides the data from the client-side cache or from the server.

// opportunitiesOverAmount.js
import { LightningElement, api, wire } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import getOpptyOverAmount from '@salesforce/apex/OpptiesOverAmountApex.getOpptyOverAmount';
import updateOpptyStage from '@salesforce/apex/OpptiesOverAmountApex.updateOpptyStage';

export default class OpportunitiesOverAmount extends LightningElement {
    @api amount = 500000;

    @wire(getOpptyOverAmount, { amount: '$amount' })
    opptiesOverAmount;

    handleClick(e) {
        updateOpptyStage({
            amount: this.amount,
            stage: 'Closed Won'
        })
        .then(() => {
            refreshApex(this.opptiesOverAmount)
            .then(() => {
                // do something with the refreshed data in this.opptiesOverAmount
            });
        })
        .catch((error) => {
            this.message = 'Error received: code' + error.errorCode + ', ' +
                'message ' + error.body.message;
        });
    }
}

The code calls refreshApex() in the handleClick method. Changing the stage to Closed Won impacts the CloseDate field, which means that the @wire data is stale. The Close Date field changes to today's date when you have a close date in the future.

To tell the system that the data is stale, call refreshApex(). The wire adapter marks its cache as stale, requeries the server for updated data, updates its cache, then notifies its subscribers. When that happens, the component's this.opptiesOverAmount property updates, which triggers a rerender with the new data. The close date is now July 18, 2019 (today’s date) and the opportunity stage is now “Closed Won”.

Shows a list of opportunities that are marked as Closed Won.

The OpptiesOverAmountApex class contains the getOpptyOverAmount method, which retrieves a list of opportunities over the specified amount. It also contains the updateOpptyStage method, which updates the opportunity stage for opportunities over the specified amount to “Closed Won” using the update DML operation.

public with sharing class OpptiesOverAmountApex {
    @AuraEnabled(cacheable=true)
    public static List<Opportunity> getOpptyOverAmount(Decimal amount) {
        return [SELECT Id, Name, Amount, StageName, CloseDate FROM Opportunity WHERE Amount > :amount];
    }

    @AuraEnabled
    public static void updateOpptyStage(Decimal amount, String stage) {
        for (List<Opportunity> oppts:
            [SELECT Id, Name, Amount, StageName, CloseDate FROM Opportunity WHERE Amount > :amount]) {
                for(Opportunity o : oppts) {
                    o.StageName = stage;
                }
                update oppts;
            }
            return;
    }
}

Example: Refresh the Cache for a Wired Function

To refresh a wired function, pass the argument the wired function receives (which is the wired value) to refreshApex(). In this sample code, the wired function is wiredGetActivityHistory(value). Hold on to the value provisioned by the wire service and pass it to refreshApex().

Call refreshApex() imperatively using a new function. To track the provisioned value, define a new property, wiredActivities, and use it in wiredGetActivityHistory(value). Destructure the provisioned value to extract the data and error objects easily.

import { LightningElement, api, wire } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import getActivityHistory from '@salesforce/apex/GalleryApexController.getActivityHistory';

export default class GalleryApexMaster extends LightningElement {
    @api recordId;
    wiredActivities;

    @wire(getActivityHistory, { accountId: '$recordId', max: '500' })
    wiredGetActivityHistory(value) {
        // Hold on to the provisioned value so we can refresh it later.
        this.wiredActivities = value; // track the provisioned value
        const { data, error } = value; // destructure the provisioned value
        if (data) { ... }
        else if (error) { ... }
        ...
    }

    handleLogACall() {
        // Use the value to refresh wiredGetActivityHistory().
        return refreshApex(this.wiredActivities);
    }
}

Example The ldsDeleteRecord component in the lwc-recipes repo calls refreshApex().

No comments:

Post a Comment