10

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)?

2 Answers 2

10

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.

2
  • You are quite right. I agree with having a thin apex layer and one source of truth to data. Actually when we communicate between apex and clientside we always communicate via an DatamodelVO which provide initial data encapsulation.(unless we are dealing with lightning-data-service) Because it is hard to manipulate field names and especially when it comes to namespaces and custom objects from other managed packages.
    – bluecat
    Commented Aug 8, 2019 at 16:31
  • @bluecat, pls mark the answer as solved if solved Commented Aug 21, 2019 at 5:31
2

So, I was struggling with this too.
I had a record that I was passing around which was being used by several components, and I didn't want to rewrite the same helper code in multiple places. I also didn't like having to document all the elements of this record in each of my components that were using it (since when I made a change, I now had to go to each of those components and make the change in the docs) Also, there was a number of common methods I was using to handle this record in each component that I didn't want to replicate:

I created a Javascript service class ("SomeService.js"), much like we commonly do with Apex. First I included a constructor whose Input Arg is a result from an Apex method. Now any component can construct the same object using the same Apex input. Next, I added some "private" methods that were used only in the Service class. (You'll see at the end, that the private methods are excluded from my "Service" object) Then I included other methods that were for local and public use by other components. Now all my various components can be working with the same record(s), and execute the same methods on that record and the code is not replicated. Note the "public" methods are all declared in the "SomeService" const, which is exported. Now, all I need to do, to use any method in this Service class, is to put a single import

import { SomeService } from 'c/someService';

And I can refer to any method in my class with the exported prefix: as in:

let myObject = SomeService.constructMyObject(apexResult); 

Joila! No more code duplication and one unified record type shared by existing and future components. This helped me organize my code very efficiently.

someService.js:

        const constructMyObject = (apexResult) => {
           let myRecord = {
                    "prop1"             : apexResult.prop1,
                    "prop2"             : fooPrivate( apexResult),
                    "prop3"             : bar( apexResult.fieldx),
                    "etc"               : "Default"
                }
                return upload;
        }       
        const fooPrivate = (something) => {
         return 'foo'; 
        }
        const bar = (somethingElse) => {
         return 'bar';
        }
        const methodX(myRecord) => {
         localRecord = {...myRecord};
         // Do stuff to localRecord
         return localRecord;
        }
   const SomeService = {
      constructMyObject: constructMyObject,
      bar: bar,
      methodX: methodX,
   }

   export {SomeService};
3
  • 1
    Yes, that is what we did at the end we move the data convert related to UI component to the clientside. We have a bunch of converter classes to do data mapping which is unit tested and reusable. And it make sense to have them on the UI side since no Apex class is going to use a UI data converter.
    – bluecat
    Commented Aug 14, 2019 at 23:12
  • 1
    One thing we found in our data converter, we need to do an object spread most of the when we converting data since the data from apex methods are not mutable by default. like we you did in "methodX(myRecord)". And when we are return a complex object or a list of complex object it is not fun to deep clone the object....
    – bluecat
    Commented Aug 14, 2019 at 23:14
  • @bluecat Also, you don't have to repeat the same documentation for the myRecord everywhere. Another thing we did was add "local" elements to the myRecord which were only used in specific components. Made it much easier when we had to use a list of the "myRecords" in an iterator.
    – DaveS
    Commented Aug 15, 2019 at 4:38

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .