In this SharePoint Framework (SPFx) quick tip, I want to focus on a question I commonly see pop up in forums as well as with new and experienced students of our course. This applies to every type of SPFx component that developers can create, including web parts, all types of extensions, libraries, and Adaptive Card Extensions (ACEs).
It builds down to the following question: How do you properly initialize your component? Should you use the object’s constructor, or the onInit()
method?
👉 You should always use the
onInit()
method if your initialization process needs anything in the SharePoint Framework API or in the current page’s context. Otherwise, you can safely use the constructor in your component.
Let’s explore why…
TL;DR
Often times, we as developers need to set up our component for a first-run experience. Maybe our component needs to show a default view before it fetches information from an external data source.
Maybe it needs to load some data based on some context, like who the current user is, or the time of day.
Whatever it is, this is a common task is easily addressed typically using a constructor.
For example, here’s what an object looks like that I use on my site when someone executes a search query:
export class Something {
constructor() {
if (TEST_MODE) { return; }
const searchTerm = this.getSearchTerm();
this.executeSearchQuery(searchTerm)
.then((searchResults) => {
const renderedResults = this.renderSearchResults(searchResults);
const displayResults = this.displayResults(renderedResults);
this.attachResultSelectHandler();
});
}
public executeSearchQuery(term): Promise<ISearchResults> { .. }
public renderSearchResults(searchResults): IRenderedResults { .. }
public displayResults(renderedResults): boolean { .. }
public attachResultSelectHandler(): void { .. }
}
When this client-side component is created on the search results page, the constructor submits the query to my search engine’s REST API, renders the results, and attaches an event handler to each item on the page.
In this case, the constructor works perfectly because I don’t have any dependencies to other JavaScript libraries on the page.
But… in the case of SPFx components, that’s not necessarily true…
What makes the SharePoint Framework different?
When it comes to components you’re writing for the SharePoint Framework, you’ve got something else you need to consider.
When a SharePoint page loads that uses the SPFx, the module loader starts downloading all the things it needs for the page. That includes the SPFx runtime and the bundles for the components included on the page, as well as each component’s dependencies.
As the following UML sequence diagram shows, all of this is happening asynchronously for performance reasons.
As each component bundle is downloaded (indicated by #2 in the image above), the SPFx creates an instance of the component and adds it to the page. But, the important thing to note here is that just because your component has finished loading and been added to the page, that doesn’t necessarily mean that SPFx has finished loading everything it needs (indicated by #3 in the image above), nor has it finished its initialization process.
In other words, when SPFx creates an instance of your component and adds it to the page (#2 in the image above), your constructor is executed, but if that uses something from the SPFx API, that thing you use may not be ready. The SPFx API isn’t ready for use until the end of its init phase (indicated by #4 in the image above). For example, if you reference the this.context
in your constructor, you might get a runtime undefined
error.
This is a great example of a race condition. Your initialization process has a dependency on the SPFx’s initialization process. If your component’s initialization starts running before the dependency’s initialization process is complete, you might have an issue. But then again, you might not because your component might run after SPFx is finished.
Even more frustrating is when it works one way in development when everything is local and faster vs. production. Or worse, when it is inconsistent in production!
To address this, the base component type that all components derive from in the SPFx has an override-able method onInit()
. When SPFx is ready (once it completes #4 in the image above), it then goes to each component and calls it’s onInit()
method. When this async method completes, SPFx will then call the render()
method on the component.
Revisiting our search example for the SharePoint Framework
Let’s take that same scenario above and port it to SharePoint. In this scenario, I’d likely want to use the SharePoint REST API to query the search engine in SharePoint.
To do that, I’d use the SPHttpClient object to submit the REST request from my component. But, as I explained above, if I used the same process as I show in my sample code that I use on my site, doing this in the constructor is risky.
For my site, there’s no runtime on the page… I don’t need to worry about something else loading on the page because I’m going to use the browser’s native Fetch API to submit a request to the anonymous REST API for my search service.
But… in the case of SharePoint, to use the SPHttpClient object, I’ll access it from the page’s context:
const results = await this.context.spHttpClient.get(endpoint, config);
As I explained above, the current page context may not be available on the page when my component initializes. So if I did the following, I might get an error due to the race condition I mentioned:
export default class MySearchResultsWebPart {
let searchResults;
constructor() {
const endpoint = ...;
this.searchResults = await this.context.spHttpClient.get(
endpoint,
SPHttpClient.configurations.v1
);
}
public render(): void { .. }
}
The safe way to do this is to get the results within the onInit()
method because that is called by the SPFx once the page has been initialized:
export default class MySearchResultsWebPart {
let searchResults;
protected onInit(): Promise<void> {
const endpoint = ...;
this.searchResults = await this.context.spHttpClient.get(
endpoint,
SPHttpClient.configurations.v1
);
Promise.resolve();
}
public render(): void { .. }
}