I’ve wrote a few blogs about the (Preview) Power Apps Grid customized control: a way to host PCFs right inside the grid. The last one in that direction was about fetching related records. It’s amazing that we are able to work with child records right inside the Power Apps Grid, but the way shown in the last blog was by making a fetch for every record and cache them. That’s not the best, in terms of API calls, but I didn’t know better.

But Jan Hajek posted a very interesting comment on my LinkedIn post. His idea was to “batch” the requests and resolve the single promises after the common request was done. The magic word was “debounce”. Sounded very promising, so I had to try it out!

The implementation of the batched debouncing

To implement the “batched requests debouncing”, I’ve created a class: the RequestManager. It’s job is to take care of the cache and debounce the requests. It’s a reusable class, and the parameters are: the fetchXml, the name of the table to be fetched, the name of the id-return column (“parentId” in my case), and the webAPI. But first I had to figure out how to wait for more ids before starting the request.

Debouncing in general

In case you are new to debouncing, a few words about it… (otherwise skip this please)

“Debouncing enforces that a function not be called again until a certain amount of time has passed without it being called. As in “execute this function only if 100 milliseconds have passed without it being called.” ( read more… )

The debouncing is a well-known technique for front-end developers. For instance if a time-consuming function (like making requests) needs to be called when the user types in a text-box, you want to make the requests only when the user stops typing. The typical implementation of a debounce: using setTimeout. Every time the function is called, the older timeout is cleared, and a new timeout starts. When the user stops typing, the function gets executed.

Here is an example of debounce implementation, from freecodecamp

function debounce(func, timeout = 300){
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => { func.apply(this, args); }, timeout);
  };
}
function saveInput(){
  console.log('Saving data');
}
const processChange = debounce(() => saveInput());

There is also a useDebounce hook: https://usehooks-ts.com/react-hook/use-debounce

Debouncing of a batch of requests

The classic debouncing I knew until now, is delaying the execution, and the last call of the function will have the last parameter passed to the function. When a user types in a text-box, you want to call the function with the text introduced (last call). But I needed to know all the ids passed while debouncing.

It sounds complicated, but it turns out that it’s not so hard to implement. The magic is made using a very nice npm package: debounce-promise. This debounce library has a few options, one of them is {accumulate: true}. That means, that when the function will be called, it will get all ids passed until then. For an example in the package docs, have a look here: https://www.npmjs.com/package/debounce-promise#with-accumulatetrue

So the RequestManager fetch method is a debounced version of the retrieveRecords, with a timeout of 200ms and {accumulate: true}.

  private debouncedAccumulatedFetch = debounce(async (ids)=>{           
      return this.retrieveRecords(ids);
    }, 200, {accumulate:true});

While the retrieveRecords will execute the fetch for all ids at once.

 private retrieveRecords = async (ids: string[]) => {
     //first create the condition for the fetch based on "values in"
      const condition = ids.map((id : string)=>`${id}`).join("");  
      console.log(`%cFetching ${condition}`, "color:#EC407A");
      const response = await this.webAPI.retrieveMultipleRecords(this.baseEntity, 
                        "?fetchXml=" + this.baseFetchXml.replace("${IDS}", condition)); 
      //all responses are cached
      response.entities.forEach((entity)=>{
        this.cache[entity[this.aliasParentId]] = entity.count;        
      });
      //return values as an array in the order of the ids passed 
      return ids.map((id)=> {
        //first I need to set the cache on 0, for every record that was not returned by the fetch
        if(this.cache[id]==null){
          this.cache[id] = 0;
        }
        return this.cache[id];
      });
    }

When the fetch was executed, first I’ll save all the results in the cache. Then I’ll return the array with the results corresponding each id. Since the fetch will return only results for the existing records, I need to set the cache on 0 for all the other records.

The public method or RequestManager is returning a Promise for the requested id. Of course, when the value is already in the cache, I don’t need make any requests.

public getRecords(id: string){          
      if(Object.hasOwn(this.cache, id)){
        return Promise.resolve(this.cache[id]);
      }
     return this.debouncedAccumulatedFetch(id);     
    }  

The request inside the React component

Inside my react component I only need to call the RequestManager.getRecords. The Promise will be resolved with the response corresponding for the row id.

  React.useEffect(() => {
        if(!parentId){
            return;
        }         
        requestManager.getRecords(parentId)
        .then((c) => {             
            if(mounted.current){
               //do something with the response here
            }
        });
    },[parentId]);   

Using a common RequestManager instance

Of course, all the React components used, need the same instance of the RequestsManager. I create it inside the CellRenderOverrides (which is a closure, returning the cell renderer):

Here you can also see my fetch, and the placeholder for the IDS which will be provided after debouncing.

This one will be called inside the index.ts

public init(
        context: ComponentFramework.Context,
        notifyOutputChanged: () => void,
        state: ComponentFramework.Dictionary
    ): void {        
        const eventName = context.parameters.EventName.raw;    
        if (eventName) {
            const paOneGridCustomizer: PAOneGridCustomizer = { 
                cellRendererOverrides: generateCellRendererOverrides(context.webAPI)             
            };
            (context as any).factory.fireEvent(eventName, paOneGridCustomizer);            
        }  
    }

The complete code for this PCF can be found in my GitHub repository brasov2de/GridCutomizerControl -> RelatedRecords_Batched

The use-case

In the last blog I’ve shown how to retrieve the users associated to a table and show them inline. This time I’ve decided to aggregate the open tasks and show them in a column. On click on this cell, I can show the list of tasks. (here opened in the Side Pane).

Maybe you don’t need the Power Apps Grid customizer PCF for that. Also in the Wave 2 is announced that the Power Apps Grid will introduce nested records. This use-case is just an example about how to use the debounced fetch of related data.

Have a look to this short recording: with 2 fetches all the records are retrieved and can be served from the cache after that. In the video below, the yellow logs means that the cells are rendered (calls on getRecords methods), the pink ones are the real fetches, and the green ones are the responses.

Show the related tasks: Side Panes vs Dialogs/ Forms vs Custom Pages

The user can click on the counter cell, and there can see the related records (tasks). I think there are 2 good ways to show the related records:

  1. Open the form for the parent table, and directly navigate to the tab containing a subgrid with tasks
  2. Create a CustomPage which is showing the tasks, with the option to create/edit/complete/delete the tasks.

The CustomPage takes a little more time to develop, but it would be the most UX/UI optimized option. For this blog I’ve decided to go with a Form.

I also have a few options to show the form (would be similar for the CustomPage):

  1. Navigate to the form
  2. open the form as a dialog
  3. open the form as a Side Pane

I’ve decided to go with option 3: Side Panes. This is the most user-friendly option, but it means that the user needs to refresh the grid by herself/himself.

To open a SidePane (or a Dialog) is technically “kind of” unsupported inside a PCF, but I consider one of the functions that are not that dangerous to use. Here is the code to open the account form on a SidePane, and directly navigate to the tasks tab. I will reuse the Side Pane named “PAGRelatedActivitiesPane” if it already exists.

const openSidePane = async (parentId: string) => {
    const paneConfig = {
        title: "Related Activities",
        imageSrc: "WebResources/sample_reservation_icon",
        paneId: "PAGRelatedActivitiesPane",
        canClose: true, 
        width: 600
    };
    // eslint-disable-next-line no-undef
    const pane1 = Xrm.App.sidePanes.getPane("PAGRelatedActivitiesPane") 
                              ?? await Xrm.App.sidePanes.createPane(paneConfig); 
     
     pane1.navigate({
        pageType: "entityrecord",
        entityName: "account",  
        entityId: parentId, 
        formId : "b8c2f260-36e8-4439-bdaa-47dd4a7593db",
        //need a tab named "tab_actions" on my account form in order to work
        tabName: "tab_activities"
        }); 
}

See it in action in the video below:

Unfortunately there is no good way to refresh the cache after the changes are done inside the SidePanes. The user needs to make the refresh.

The other option would be to open a dialog

 const onClick = React.useCallback(() => {      
        if(parentId){       
            //openSidePane(parentId);  
            openDialog(parentId, context).then(()=>{
                requestManager.refresh(parentId).then((c)=>{
                    if(mounted.current){
                        setCount(c ?? 0);
                    }
                })
            })
        }
    }, [parentId]);

This time I can refresh the record right away. The downside: a fetch will be executed after each Dialog.

Conclusion

By debouncing the requests, the performance gets better and the API consumption too. Thank you Jan Hajek for this idea! This community rocks!

In case the data gets changed outside the grid, it’s harder to refresh by collecting the requests. That’s because the cells are not allways refreshed when you scroll up and down. It’s possible to implement some kind of cached-dirty-tracker , but you have no control when the cells will be refreshed. That could cause confusion for the user, so it’s better to refresh per row or to let the user refresh the grid by herself/himself.

Photo by Felipe Santana on Unsplash

Debounce Sources:

Advertisement