LWC – Best Practices for Data Service Architecture

While developing lwc components and lightning apps I always run into this design dilemma: When I have two children components that uses the same set of source data but in different forms what is the best practice when it comes retrieving data?

Here is an example situation that I run into recently:

I want do display a list of parcels for a city

Parent Component:

Child Component 1: a table view of the parcels which display all the parcels in columns. columns are defined based on a list view fields.

Child Component 2: a map view that plotted the parcels on the map based on its geo-location. And also display a summary of the parcel object. the summary is also generated based on the same list view fields.

Child Component 1 need know a list of parcel and fields from a list view and convert them into table data format.

Child Component 2 need to know the same list of parcel and list view and convert them into mapviewer data format.

My struggling is to find a best practice to sufficiently retrieve the data. and scalable way to transform the data so that the two children component are not coupled and they can be used independently in the future.

Approach 1:
Child Component 1 and Child Component 2 are developed separately and they are self-contained. So CC1 and CC2 have their own apex controllers and wiring functions. The controllers uses the same Apex DataService to retrieve required information: parcels and list view. The controllers uses their own data converter to convert parcels and listview in to the data format required by UI(like DataTableVO and MapFeatureVO). And these data modules are returned to the client for the use of UI display.

Advantages:

  • the child components are highly decouple
  • DataTableVO and MapFeatureVO are in the apex code, it is easily to exposes these classes for global use and increased the readability of the code. Consumers knows how to implements a convertor for these data type increase the exdtenbilty of these component

Disadvantages:

  • When we integrate two component together, since they are not binds to same datasource on the UI it is hard to sync data between two component. One change on CC1 cannot reflect on CC2 via data binding.
  • Since the data convertor is in Apex when new data(say a new parcel) is added via UI, client logic cannot do the conversion it always need to go back to Apex to refresh data
  • too many duplicate and inefficient web calls. (ex. retrieving same data set two times when parent component is loaded).

Approach 2:
Child Component 1 and Child Component 2 requires Higher Order Component to feed the data they need. And data are passed on as @api properties. Parent Component retrieves required data(parcels and list view info) via apex wire. Have javascript helper functions to convert data into the format that subcomponents needs. After data has been parsed and converted set the binding @api properties on sub components and make them render.

Advantages:

  • Child components are almost pure ui component more extendable.
  • Data management are more centralized. easy to handle data sync via parent component and data binding
  • Efficient in terms of web calls.
  • More logics on the client side and the data convertors are close to where the data is consumed(Sub component).

Disadvantages:

  • Child component is not self-contained. You always need to provider a higher order components when you try to use the child component.
  • DataModel VO and converter is in javascript and it is not type safe. Decreasing the readability of the code(maybe)?

Best Answer

Firstly, a very nicely framed question!

Here is what we found to be best after brain-storming:

  1. Apex methods should be used ONLY for getting the data and for DML statements. In short, it should be used only as a communication layer between component and database and nothing more - no more modification of data in apex. All the modifications to the data-structures should be done in client side.

  2. There should be a single source of truth to data - always. So, you should be getting the data in the parent component, then do the necessary modifications to the data and create an object something like:

    this.mainData = {
        actualData: [{},{},...{}], // data from server
        tableData: {
            columns:[{},...{}],
            otherAttributes: {}
        },
        mapData: {
            someAttributes: {}
        }
    }
    
  3. You can pass either the mainData or the needed data like mainData.tableData to child components.

  4. Whenever any data change is made in child components, send that change in custom event, and the parent component should handle it thereby automatically passing the data down the hierarchy. Remember that in any case child components cannot modify api properties, they should be working on the cloned properties.


Child component is not self-contained. You always need to provider a higher order components when you try to use the child component.

Not all components can be self-contained completely. They will be either data-self-contained or UI-self-contained. So, this is totally fine in terms of scalability and readability.


DataModel VO and converter is in javascript and it is not type safe. Decreasing the readability of the code(maybe)?

When you are directly returning the database objects, there will be no problem as you have to use the API names of objects/fields everywhere in client side HTML/JS.

But when you have to get the data from multiple sources, you can create a separate class. This class will have all the properties needed and separate methods to define each data-type. Consider below class:

global class pocMyData {

    @AuraEnabled global String Id{get;set;}
    @AuraEnabled global String accName{get;set;}
    @AuraEnabled global String conName{get;set;}
    @AuraEnabled global String description{get;set;}
    @AuraEnabled global String datatype{get;set;}
    @AuraEnabled global String otherField{get;set;}

    public static pocMyData getMyDataType1(sObject sobj, sObject otherObj) {
        Account acc = (Account)sobj;
        Contact con = (Contact)otherObj;
        pocMyData pocInfo = new pocMyData();
        pocInfo.datatype = 'accMain';
        pocInfo.Id=acc.Id;
        pocInfo.accName=acc.Name;
        pocInfo.description=acc.description;
        return pocInfo;
    }
    public static pocMyData getMyDataType2(sObject sobj, sObject otherObj) {
        Account acc = (Account)sobj;
        Contact con = (Contact)otherObj;
        pocMyData pocInfo = new pocMyData();
        pocInfo.datatype = 'conMain';
        pocInfo.Id=con.Id;
        pocInfo.conName=con.Name;
        pocInfo.description=acc.description;
        return pocInfo;
    }
}

Here I have the ability to have 2 data-types from the mix of Account and Contact. So when I try to get the datatypes by using:

Account acc = [SELECT Id, Name, Description FROM Account WHERE Id='00128000009j45sAAA'];
Contact con = [SELECT Id, Name FROM Contact LIMIT 1];

System.debug('getMyDataType1 => '+pocMyData.getMyDataType1(acc,con));
System.debug('getMyDataType2 => '+pocMyData.getMyDataType2(acc,con));

I get below:

getMyDataType1 => pocMyData:[Id=00128000009j45sAAA, accName=University of Boston, conName=null, datatype=accMain, description=University of BostonModified from code, otherField=null]

getMyDataType2 => pocMyData:[Id=00328000008ZUISAA4, accName=null, conName=Rose Gonzalez, datatype=conMain, description=University of BostonModified from code, otherField=null]

If you observe above, I know from datatype whether its accMain or conMain. In this case, the properties will become the API names for the client side components.

Now, when you convert the accounts and contacts using this global wrapper, your code will be readable and error-free in client side as the API names have single source of truth.