top of page

Create Re-usable Services in SharePoint Framework (SPFx)

SPFx (at least when I started back in November/December 2017) is very component oriented — you can do web parts or extensions. The components can be re-used easily enough but they almost always have a UI aspect to them. I had a lot of service type functions that have no UI and didn’t fit well into the web part / extension tooling SPFx provides.


I solved it by creating services as singletons. The following example portrays a “workflow service.” This service knows how to iterate over available SharePoint worfklows, start them, etc. Here’s singleton part of it:

import SPHttpClient from '@microsoft/sp-http/lib/spHttpClient/SPHttpClient';
import WebPartContext from '@microsoft/sp-webpart-base/lib/core/WebPartContext';
import {ConfigService} from '../../framework/services/ConfigService/ConfigService';

export class WorkflowService
{
    private static instance: EGWorkflowService;
    
    private constructor(private context: WebPartContext)
    {
        this.initialize();
    }
    
    public static getInstance()
    {
        if(!WorkflowService.instance)
        {
            throw "WorkflowService.ts: This service must first be 
            initialized using initializeInstance(context).";
        }
        return WorkflowService.instance;
    }
    
    public static initializeInstance(context: WebPartContext)
    { 
        if(!WorkflowService.instance)
        {
          EGWorkflowService.instance = new EGWorkflowService(context);
    }
    return WorkflowService.instance;
}
// Workflow service methods go here.

This follows the Singleton pattern. Here’s where you can look at that in more detail: https://stackoverflow.com/questions/30174078/how-to-define-singleton-in-typescript

I have to import SPHttpContext because I almost always use it in my services. And since these are individual “plain” TS files, they don’t benefit from the auto-wiring you get from an SPFx web part.


Here’s how I initialize a service from a web part:

public onInit(): Promise<void>
{
    return new Promise<void>((resolve,reject)=>
    {
        DocumentsService.initializeInstance(this.context);
        WorkflowService.initializeInstance(this.context);
        resolve();
    });
}

In the above gist, you can see that I’m calling initializeInstance() on two services, “DocumentsService” and “WorfklowService.”


You can do this safely do this even if the web part is on the page more than once or if multiple web parts do this on the same page. This is case where it’s nice that JavaScript is single threaded.


For me, I had to do this in the web part container as opposed to the React component itself:


(Again, because I need to pass down the http context and this is where it’s easily available; I meant to implement a more elegant approach for this, but it was so easy to do it like this that it just sort of “stuck.”)


And finally, a React component (or even another service) that needs to invoke methods on the service works like this:

private async handleConfirmAdvanceWorkflow()
{
    this.setState(
    {
        isAdvancingWorkflow: true,
        didAdvanceWorkflow: false,
        wasErrorAdvancingWorkflow: false
    } as DraftResearchApproveOMaticState);
    
    console.log(`DraftResearchApproveOMatic.tsx: 
    handleConfirmAdvanceWorkflow: publishing the page!`);
    
    try
    {
        const wfToRun = this.state.isAwaitingManagerApproval ? 
        this.props.managerSubmittedWorkflowName : 
        this.props.analystSubmittedWorkflowName;
        
        constwfService=WorkflowService.getInstance();
        
        const wfResult = await wfService.startWFByDisplayName(
        {
            forWorkflowName: wfToRun,
            onSharePointListTitle: "Draft Research",
          onSPItemID: this.state.selectedDraftResearchItem.SharePointID});
        
        this.setState(
        {
            isAdvancingWorkflow: false,
            didAdvanceWorkflow: true,
            wasErrorAdvancingWorkflow: false,
            isConfirmingApproval: false,
            selectedDraftResearchItem: null
        } as DraftResearchApproveOMaticState);
    
    // this.loadDraftResearchItem(); 
    // reload it to get latest
    
    }
    catch(publishingError)
    {
        const  errorDetails=
        {
            msg: "ApproveOMatic: handleConfirmPublishPage: Sorry! 
            Couldn't publish the item. You can try to publish it 
            manually",
            serviceError: publishingError,
            state: this.state,
            props: this.props
        };
        
        this.setState(
        {
            wasErrorLoading: true,
            isLoadingDraftDocument: false,
            didLoadDraftDocument: false,
            loadingErrorDetails: errorDetails
        } as DraftResearchApproveOMaticState);
    }
}


At line 17, I invoke getInstance() on the WorkflowService. This doesn’t throw an error since it was already initialized by the container.


At line 19, I invoke the “startWFByDisplayName” method on the service.


This approach has worked out very well. It’s easy to reason about, easy to create new services and easy to know when something goes wrong (usually because I’d try to getInstance() on a service that hadn’t been initialized).


I put all of my re-usable services in a “Services” folder:


It’s important to keep the “singleton” nature of these services in mind. The main implication is that they share one set of data per page load and consequently, really need to be stateless. This was never a problem for me, although in the early goings-on, I did forget this and ran into some difficult-to-debug issues before I fully groked the implications. It actually benefited performance — a service that retrieves a user’s profile can do it just once and return the same profile to any web part that needs it (which in my case, we almost every web part on every page). I may write a separate short blog post on this topic later.


Hope this is useful! Microsoft will likely (if they have not already), provide some framework support for service type features. In the meantime, this does the trick.




Source: Medium - Paul Galvin


The Tech Platform

bottom of page