With SPFx solutions getting more complex day by day and with lots of components to manage, passing the web part (or extension) context around to different parts of your code can get difficult to maintain real fast.
The problem:
For example, imagine we have created a custom service which needs the MSGraphClient to make a call to the Microsoft Graph and we are consuming this service in our SPFx webpart. To initialise the service, we need to either pass in the entire web part context to it, or explicitly pass in the MSGraphClient object from the context.
In the first case, we are unnecessarily passing in all other objects in the context to this service as well.
And in the second case, our code becomes tightly coupled i.e. in the future, if our service needs something else from the webpart context, we have to update the service as well as the consuming code (i.e. the webpart) to pass in the new dependency.
The solution:
Using the Service Locator pattern in SPFx through service scopes, we can abstract away the implementation details of our custom services from the calling code (i.e. webparts, extensions).
With service scopes, we can get hold of instances of SPFx classes like MSGraphClient, AadHttpClient, SPHttpClient and PageContext from our services without having to explicitly pass them in from the webparts.
In this post, lets see how to achieve this:
1) Calling the MSGraphClient from a custom service:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ServiceKey, ServiceScope } from '@microsoft/sp-core-library'; | |
import { MSGraphClientFactory, MSGraphClient } from '@microsoft/sp-http'; | |
export interface ICustomGraphService { | |
getMyDetails(): Promise<JSON>; | |
} | |
export class CustomGraphService implements ICustomGraphService { | |
//Create a ServiceKey which will be used to consume the service. | |
public static readonly serviceKey: ServiceKey<ICustomGraphService> = | |
ServiceKey.create<ICustomGraphService>('my-custom-app:ICustomGraphService', CustomGraphService); | |
private _msGraphClientFactory: MSGraphClientFactory; | |
constructor(serviceScope: ServiceScope) { | |
serviceScope.whenFinished(() => { | |
this._msGraphClientFactory = serviceScope.consume(MSGraphClientFactory.serviceKey); | |
}); | |
} | |
public getMyDetails(): Promise<JSON> { | |
return new Promise<JSON>((resolve, reject) => { | |
this._msGraphClientFactory.getClient().then((_msGraphClient: MSGraphClient) => { | |
_msGraphClient.api('/me').get((error, user: JSON, rawResponse?: any) => { | |
resolve(user); | |
}); | |
}); | |
}); | |
} | |
} |
Consuming the custom service from an SPFx webpart:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
CustomGraphService | |
} from '../../services'; | |
//In the SPFx webpart (or extension) where this.context is the webpart context. | |
const _customGraphServiceInstance = this.context.serviceScope.consume(CustomGraphService.serviceKey); | |
_customGraphServiceInstance.getMyDetails().then((user: JSON) => { | |
console.log(user); | |
}); |
2) Calling the AadHttpClient from a custom service:
Similarly, we can also use service scopes to get hold of an instance of the AadHttpClient class. In the code below, to keep things simple, I am using the AadHttpClient to make a call to the Microsoft Graph. Eventually, the result is the same as the previous code but it should demo how to use the AadHttpClient through service scopes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ServiceKey, ServiceScope } from '@microsoft/sp-core-library'; | |
import { AadHttpClientFactory, AadHttpClient, HttpClientResponse } from '@microsoft/sp-http'; | |
export interface ICustomService { | |
executeMyRequest(): Promise<JSON>; | |
} | |
export class CustomService implements ICustomService { | |
public static readonly serviceKey: ServiceKey<ICustomService> = | |
ServiceKey.create<ICustomService>('my-custom-app:ICustomService', CustomService); | |
private _aadHttpClientFactory: AadHttpClientFactory; | |
constructor(serviceScope: ServiceScope) { | |
serviceScope.whenFinished(() => { | |
this._aadHttpClientFactory = serviceScope.consume(AadHttpClientFactory.serviceKey); | |
}); | |
} | |
public executeMyRequest(): Promise<JSON> { | |
//You can add your own AAD resource here. Using the Graph API resource for simplicity. | |
return this._aadHttpClientFactory.getClient("https://graph.microsoft.com").then((_aadHttpClient: AadHttpClient) => { | |
//This would be your custom endpoint | |
return _aadHttpClient.get('https://graph.microsoft.com/v1.0/me', AadHttpClient.configurations.v1).then((response: HttpClientResponse) => { | |
return response.json(); | |
}); | |
}); | |
} | |
} |
Consuming the custom service from an SPFx webpart:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
CustomService | |
} from '../../services'; | |
//In the SPFx webpart (or extension) where this.context is the webpart context | |
const _customServiceInstance = this.context.serviceScope.consume(CustomService.serviceKey); | |
_customServiceInstance.executeMyRequest().then((user: JSON) => { | |
console.log(user); | |
}); |
3) Calling the SPHttpClient from a custom service:
And finally, if we just want to make a call to the SharePoint REST API from our SPFx code, we can also use service scopes to get hold of an instance of the SPHttpClient class. The following code also demonstrates how to get an instance of the PageContext class to get different run time context values like the current web url. Using the current web url, we can make a call to the SharePoint REST API to get the web details:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ServiceKey, ServiceScope } from '@microsoft/sp-core-library'; | |
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; | |
import { PageContext } from '@microsoft/sp-page-context'; | |
export interface ICustomSPService { | |
getWebDetails(): Promise<JSON>; | |
} | |
export class CustomSPService implements ICustomSPService { | |
public static readonly serviceKey: ServiceKey<ICustomSPService> = | |
ServiceKey.create<CustomSPService>('my-custom-app:ICustomSPService', CustomSPService); | |
private _spHttpClient: SPHttpClient; | |
private _pageContext: PageContext; | |
private _currentWebUrl: string; | |
constructor(serviceScope: ServiceScope) { | |
serviceScope.whenFinished(() => { | |
this._spHttpClient = serviceScope.consume(SPHttpClient.serviceKey); | |
this._pageContext = serviceScope.consume(PageContext.serviceKey); | |
this._currentWebUrl = this._pageContext.web.absoluteUrl; | |
}); | |
} | |
public getWebDetails(): Promise<JSON> { | |
return this._spHttpClient.get(`${this._currentWebUrl}/_api/web`, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse) => { | |
return response.json(); | |
}); | |
} | |
} |
Consuming the custom service from an SPFx webpart:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
CustomSPService | |
} from '../../services'; | |
//In the SPFx webpart (or extension) where this.context is the webpart context | |
const _customSPServiceInstance = this.context.serviceScope.consume(CustomSPService.serviceKey); | |
_customSPServiceInstance.getWebDetails().then((web: JSON) => { | |
console.log(web); | |
}); |
As always, the code from this post is available on GitHub: https://github.com/vman/ServiceScopeTestBench