Wednesday, December 12, 2018

Using PnPJS and Async/Await to Really Simplify Your API Calls

Anyone who knows us at Sympraxis realizes that Julie (@jfj1997) is the master developer. I consider myself quite capable, but Julie is always at least three steps ahead of me. She often leads me by the nose to a better pattern or practice, and it always seems to be a good idea.

Over the last few days – at Julie’s suggestion – I finally decided to try using the PnPJS library with a couple of SPFx Web Parts I’m working on. Even though I was on all the calls in the beginning of the effort to build it, and on some level it’s a “modern” SPServices, I decided a while back that I didn’t really like the way it looked. I understand REST just fine, I figured. Why would I need a helper library?

I’ve been cruising along just fine, building great stuff for our clients, but Julie started telling me recently that PnPJS and async/await was reducing her code size significantly. It sounded too good to be true, frankly. So I figured I should give it a try.

Here are a couple of examples to demonstrate how right Julie was and how much code I’ve been writing that I didn’t need to write. (Allow me to get away without doing everything I need to in the try/catch here. That’s not the point I’m trying to make. Watch for a post on Julie’s blog about how to use PnPJS’s Logging package with try/catch.)

Calling Search to Get Sites

In this instance, switching to PnPJS and async/await didn’t greatly diminish my code size, but I do think it makes it more readable. It also runs significantly faster.

Here’s the starting code. It’s a simple call to the search endpoint requesting Sites (as in Site Collections in the old parlance) with some specific exclusions. The Web Part this is in shows all the sites the current user has permissions to see. It’s like the modern Sites Web Part, but that Web Part doesn’t allow us to:

  1. Show *all* the sites the user can access, and
  2. Control the styling of the output

The end result is a listing of the sites, showing their icon and Title as a link to the site.

This is one of the data calls as it started:

private _getSiteData(): Promise<any> {

    var thisDomain: string = location.host.split(".")[0];
    var exclusions: string[] = ["https://" + thisDomain + "-my.sharepoint.com", "https://" + thisDomain + ".sharepoint.com/portals/personal"];
    var exclusionString: string = " -Path:" + exclusions.join(" -Path:");
    exclusionString += " -Path=https://bluebirdbio.sharepoint.com";

    var url: string = this.context.pageContext.web.absoluteUrl +
      "/_api/search/query" +
      "?querytext='contentclass:sts_site " + exclusionString + "'" +
      "&selectproperties='Title,Path,SiteLogo'" +
      "&rowlimit=500";

    return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1,{
      headers: {
        "Accept": "application/json;odata.metadata=none"
      }
    })
      .then((response: SPHttpClientResponse) => {
        return response.json();
      });
  }

And this is what it looks like using PnPJs and async/await.

private async _getSiteData(): Promise<ISPSite[]> {

    var thisDomain: string = location.host.split(".")[0];
    var exclusions: string[] = ["https://" + thisDomain + "-my.sharepoint.com", "https://" + thisDomain + ".sharepoint.com/portals/personal"];
    var exclusionString: string = " -Path:" + exclusions.join(" -Path:");
    exclusionString += " -Path=https://bluebirdbio.sharepoint.com";

    try {

      let result = await sp.search(<SearchQuery>{
        Querytext: "contentclass:sts_site " + exclusionString,
        RowLimit: 500,
        SelectProperties: ["Title", "Path", "SiteLogo"]
      });

      return this.processSearchResults(result);

    } catch (e) {

      console.error(e);
      return null;

    }
    
  }

Even with the added try/catch the code is smaller, and since we’re building up the request with the library, we get help from our IDE (I use WebStorm) because the library is strictly typed. In the first example, since I’m building the request in the URL string, it’s easier to type an error into my code.

Getting List Data

In this example, I’m retrieving values from what I think of as a reference list for a more full-fledged application-in-a-Web-Part.

public getShipmentStatuses(serviceProps: IServiceProperties): Promise<IStatus[]> {
  return new Promise((resolve, reject) => {
    var urlList: string = `${serviceProps.context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('SL_ShippingStatuses')/RenderListDataAsStream`;
    var requestOptions = this.spHttpOptions.getShipmentStatuses;
    serviceProps.context.spHttpClient.post(urlList,
      SPHttpClient.configurations.v1,
      requestOptions
).then((responseList: SPHttpClientResponse): Promise<{ value: any }> => {
      return responseList.json();
    })
      .then((responseJson: any) => {
        var shipmentStatuses: IStatus[] = [];
        for (var i = 0; i < responseJson.Row.length; i++) {
          var thisRow = responseJson.Row[i];
          var thisShipmentStatus: IStatus = new Status();
          thisShipmentStatus.Id = thisRow.ID;
          thisShipmentStatus.Title = thisRow.Title;
          thisShipmentStatus.SortOrder = thisRow.SortOrder;
          thisShipmentStatus.Follows = thisRow.Follows;
          thisShipmentStatus.CanBeCancelled = thisRow.CanBeCancelled === "Yes";
          shipmentStatuses.push(thisShipmentStatus);
        }
        resolve(shipmentStatuses);
      })
      .catch((err) => {
        reject(err);
      });
  });
}

This turns into:

public async getShipmentStatuses(serviceProps: IServiceProperties): Promise<IStatus[]> {

    try {

      let items = await sp
        .web
        .lists
        .getByTitle("SL_ShippingStatuses")
        .items
        .select("Id", "Title", "SortOrder", "CanBeCancelled")
        .orderBy("SortOrder")
        .get(spODataEntityArray<Item, IStatus>(Item));

      return items;

    } catch (e) {
      
      console.error(e);
      return null;
      
    }
}

Much less dense and easier to follow, I think.

The other thing I’m taking advantage of in PnPJs here is Entity Merging. You can see how simple that is in line 12 above. Rather than building up my object in what amounts to a constructor, the Entity Merging package takes care of it for me. All I have to do is tell it what interface to use. (Above, it’s IStatus.)

Summary

Sometimes it’s important for old dogs to learn new tricks. In software development, it’s really a requirement. It didn’t take me anywhere as near as much time to take these steps forward, and I’m already seeing performance improvements and simpler code – both wins for my clients. If Julie recommends something – we should ALL listen!


by Marc D Anderson via Marc D Anderson's Blog

No comments:

Post a Comment