1

To demonstrate a specific issue with LDS cache, I have created simple lightning web component that displays list of accounts (filterable by account type) using lightning-combobox and lightning-datatable. The source code for this component can be accessed in this git repo. [Since this is a demo component, I may have skipped best practices and detailed comments, but the code is fairly easy to understand.]. High level details of the component are given below:

  • LWC uses lightning-combobox to display the account types and is used to filter the result set. Based on a user selected type value, a (un)filtered list of accounts is displayed in a lightning-datatable. The account name field is editable in the data table.
  • Component wires custom apex methods to fetch account types and list of accounts; and uses lightning\uiRecordApi to update changes made to the account name. refreshApex method is used to force refresh on LDS cached result.
  • Apex class has 2 methods - One for fetching the list of distinct Account Types and the other for fetching list of accounts based on the account type.

enter image description here

Problem Statement:

After updating a record, LDS cache is not refreshed as expected. Replication steps mentioned below:

  1. Note down the first 2 account records (account name) for the type Customer - Direct.
  2. Select Customer - Direct in the combobox to get the filtered result set.
  3. Edit the account names for the records noted down earlier and click 'Save' to update the data.
  4. Component is refreshed with the latest updated data.
  5. Select All Types in the combobox to get list of all accounts. It can be observed that the updated records do not display the latest values (but old values from LDS cache).
  6. Switch back to Customer - Direct to see the updated values.

This indicates that there are distinct LDS cache entries for each filtered list of accounts and only the cache corresponding to the current displayed result set (or the one fetched by last @wire adapter execution) is refreshed. The scenario presented here is a very simple one, but there could be slightly complex scenarios. For example, the filter values are fetched from a multi-select picklist field and the record(s) could be present n (>2) number of filtered result sets displayed in the data table. This would require all n LDS cache entries to be refreshed.

What I already know or tried so far:

  • Use imperative apex calls for accounts retrieval as well as updating and use getRecordNotifyChange (lightning\uiRecordApi) & refreshApex to have complete control over the displayed data. By taking this approach, I might have to sacrifice the usage of ui*Api methods as well as LDS cache.
  • Use the same approach as above, but mark apex methods as cacheable [@AuraEnabled(cacheable=true)]. Doing this doesn't help either because this still doesn't refresh all the cache entries.

Solution: Work-around to solve this problem is provided as answer. But I would like to know if there is any better approach (or if I missed out on a really simple logic) to solve this problem.

Note: I've tried to provide as much details as I could to illustrate the problem. Feel free to comment, if any part of this question is unclear. Also, in the solution, I may have skipped some best coding practices since that's not the problem at focus. Suggestions are welcome.

0

AFAIK, there is not much documentation on how exactly LDS manages its cache entries (other than details mentioned here, here, here and here). Chrome developer console's lightning > storage tab helps to some extent in observing the LDS cache data. In lack of documented mechanism, using JS maps as private fields within the lightning web component and tracking the LDS cache entries of interest can be used as a work-around. Code fix for this problem is provided in this git repo (includes additional code comments than provided in the code snippets below). While this answer is relevant to the simple LWC being used here, the idea can be leveraged/ manipulated to suit other complex scenarios too.

Create 2 JS Maps - cacheMap & cacheRef.

cacheMap stores the filter (or combobox values) as key & and array of (result set's) record IDs as its corresponding value. The purpose of this map is to easily locate the keys whose values have the updated record's IDs.

cacheRef stores the filter (or combobox value) as key & and the result fetched by wire adapter as its corresponding value. This serves as a direct reference to the LDS cache entry.

Sample values for these JS maps would be as shown in the screenshot below. At any given point of time, both maps will have same number of entries (with 1-to-1 mapping between the keys).

enter image description here

The idea is that if the updated record ID is found in any of the entries in the cacheMap, then pick the corresponding entry in cacheRef and pass its value to the refreshApex method (thereby, passing the reference of corresponding LDS cache entry). This would refresh the specific or all relevant cache entries.

Declare the following private fields in the LWC JS file:

// Local storage for account type as key and array of record IDs as value
cacheMap = new Map(); 
// Local storage for account type as key and data fetched by wire adapater as value.
// This map is used to store the actual reference to the LDS cache entries.
cacheRef = new Map(); 
updatedIDs = undefined; // Store the updated record IDs during every DML

Include this code in the wired apex call (to populate the maps with required values anytime the user selects a filter value):

if(result.data){
    this.cacheMap.set(this.selectedTypeLabel, 
        this.accounts.data.slice().map(rec => { 
            const fields = Object.assign({}, rec); return fields.Id; 
        })
    );
    this.cacheRef.set(this.selectedTypeLabel, this.accounts);
}

In the lightning-datatable's save handler JS method, include the following code (to capture the records being updated):

this.updatedIDs = recordInputs.map(record => record.fields.Id);

Include this JS method in the component JS and call it after the records are updated.

refresh(){
    // Array to capture the specific filter keys that resulted with updated records
    let refreshKeys = []; 

    // Loop thru the 'cacheMap' and find the filter keys have updated record's IDs
    this.cacheMap.forEach((value, key) => {
        refreshKeys.push(value.some(element => {return this.updatedIDs.includes(element);})? key : null);
    });

    // For each filter key, locate the corresponding entry in 'cacheRef' and invoke refreshApex 
    // method. Value in the `cacheRef` entry serves as reference to the LDS cache entry
    refreshKeys.forEach(async key => {
        this.isLoading = true;
        await refreshApex(this.cacheRef.get(key));
        this.isLoading = false;
    });       
} 
0

I remember facing the same situation recently in Aura too, I simply used a aura If else to re-render the specific part of the component, its more like a refresh you see ,something similar of a refresh works in LWC too ... I did this as a workaround for LWC until I find something better

eval("$A.get('e.force:refreshView').fire();");

It wont work without the eval, it will say $A usage is disallowed in LWC

3
  • How exactly did you place this in your LWC? Tried it in the sample component's (provided in the question) refresh method, but it didn't help. By definition, force:refreshView reloads the view without page reload. So, in my comp, this line caused the reload of combobox and data-table, but didn't refresh cached data . Besides, the sample component I've provided is a very simple scenario. Larger picture of this issue would be were a record is part of more than two filtered result set (i.e. filter value corresponds to a multi-select picklist field). This code won't help in such complex cases. – arut Oct 25 '20 at 9:16
  • 1
    This will only work if you were using LDS using markup like record form and all I have posted another answer that should solve your scenario – Arihant Kumar Seraogi Oct 25 '20 at 17:58
  • That's exactly the point in my question. If the component provided didn't have a combobox, then there will be only on LDS cache entry and this issue will not happen. Same is the case if use record form or anywhere you use lightning\ui*Api (because there are built with sort of singular purpose). – arut Oct 25 '20 at 18:12
0

After testing out your code and quite a bit of research I was finally able to do it, I found it in the documentation and was able to update the cache values using a function: getRecordNotifyChange()

enter image description here

https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.data_table_inline_edit

The very last example should solve your scenario

async handleSave(event){
    const updatedFields = event.detail.draftValues;
    // Prepare the record IDs for getRecordNotifyChange()
    const notifyChangeIds = updatedFields.map(row => { return { "recordId": row.Id } });
    console.log(notifyChangeIds,' : notifyChangeIds');

    const recordInputs = event.detail.draftValues.slice().map(draft => {
        const fields = Object.assign({}, draft);
        return {fields};
    });
    const promises = recordInputs.map(recordInput => updateRecord(recordInput));
    Promise.all(promises).then(() => {
        this.dispatchEvent(
            new ShowToastEvent({
                title: SUCCESS_TITLE, 
                message: SUCCESS_MSG, 
                variant: SUCCES_VARIANT
            })
        );
        console.log(notifyChangeIds,' : notifyChangeIds');
        getRecordNotifyChange(notifyChangeIds);
        // Display fresh data in the datatable
        refreshApex(this.accounts)
        // Clear all draft values in the datatable also hides the save and cancel button
        this.draftValues = [];
        
    })
    .catch(error =>{
        console.log('error: ',error);
        this.dispatchEvent(
            new ShowToastEvent({
                title: ERROR_TITLE, 
                message: error.body.message, 
                variant: ERROR_VARIANT
            })
        );
    });
}

This works perfectly the cache value gets updated issue seems with the way filtering is working just filter the data client side ..that way you can avoid two lds cache entries

6
  • I've already mentioned about this method as well as the link in my post. Could you confirm that this actually worked for you in the component provided in my question? If yes, then pls do provide the relevant updated snippet. I had tried this earlier and it actually didn't work for me [I had noted my observations in the question]. I presume it works well when all the apex calls are imperative, but that would mean not being able to use the lightning\ui*Api anywhere in the component (which is not desirable in certain cases considering component performance and optimization). – arut Oct 25 '20 at 18:36
  • edited it included my handle save function – Arihant Kumar Seraogi Oct 26 '20 at 5:13
  • Appreciate your response and effort. I did this exactly this before posting my question. It didn't work then and its not working now. I see some inconsistencies in your code. Why declare async on handleSave when you are not doing await on refreshApex or anywhere else within the function? I guess you have declared draftValues at the top of the JS file and hence, using this.draftValues = []; (but this was not defined in my component). – arut Oct 26 '20 at 6:25
  • ohh the async keyword i was still tinkering with the code ..earlier i was using await refresh function is your code...a bit of clean up is required – Arihant Kumar Seraogi Oct 26 '20 at 8:19
  • About the code inconsistencies, having async keyword doesn't really impact anything. I had mentioned it just to check if the code posted in your answer is exactly same as the one that seems to be working for you. For example, in your HTML file, you must have included draft-values attribute for lightning-datatable (without that the hiding for save/ cancel buttons won't work). Sometimes, minor differences in the JS code could cause issues in the component. – arut Oct 26 '20 at 19:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.