Handling page creation events in your SPFx application customizer
This post is over a year old, some of this information may be out of date.
Some time ago I wrote about things to check in your SharePoint Framework application customizer for making sure it behaves correctly on navigation events. This included checks to see if you navigated to another page, different hub site and site with another language.
In most cases, these are the checks that you want to put in place, to make sure that your application customizer renders the right data after a navigation event.
Info: There are already a couple of articles written about the navigation events topic for application customizers:
- SharePoint Framework Application Customizer Cross-Site Page Loading by Julie Turner
- Record page hit when SPA page transitioning in modern SharePoint sites by Velin Georgiev
- Handling navigation in a SharePoint Framework application customizer
- Things to check in your SPFx application customizer after page transitions
One more thing to check or add
There is an issue when creating a new page in SharePoint. A couple of months ago (or even longer), when you created a new page from the UI. SharePoint created a new page for you with a unique name and opened it in edit mode. Nowadays, it does it differently.
These days when you create a page, it redirects you to the following page on the site: /SitePages/newpage.aspx?Mode=Edit
. This is good, as there won’t be a new page created until you save it, or until the automated save action kicks in. This happens a couple of seconds after you gave your page a title and start editing the content zone.
Due to this change, there is no navigation event-triggered anymore when creating a new page. Which means that your application customizer will think it is still on the previous page.

As you can see in the animated GIF, the navigation event isn’t getting triggered when a new page gets created. Depending on the logic of your application customizer, you might want to know when it’s on new page creation or do something after the page has been created.
Adding an extra browser history check
As the navigation event handler will not be called during these new page creation transition, we need to find another solution. The solution for this is something I created for checking the display mode of the page from within an SPFx extension.
Related article: Check the page display mode from within your SharePoint Framework extensions
The approach used in the related article was to bind into the browser history API. As this is what SharePoint uses these days for navigating between pages. The same approach can be applied here to check if a new page gets created, and when it is saved.
/** | |
* Bind the control to the browser history to do metadata check when page is in edit mode | |
*/ | |
private bindToHistory(): void { | |
// Only need to bind the pushState once to the history | |
if (!this.pushState) { | |
// Binding to page mode changes | |
this.pushState = () => { | |
const defaultPushState = history.pushState; | |
// We need the current this context to update the component its state | |
const self = this; | |
return function (data: any, title: string, url?: string | null) { | |
console.log("bindToHistory:", url); | |
// Check if you navigated to a new page creation | |
if (url.indexOf("newpage.aspx")) { | |
// Check if the page is in edit mode | |
if (url.toLowerCase().indexOf('mode=edit') !== -1) { | |
self.isNewPage = true; | |
} else if (self.isNewPage) { | |
// Page is created | |
self.handleNewPageReload(); | |
} | |
} else if (self.isNewPage) { | |
// Page is created | |
self.handleNewPageReload(); | |
} | |
// Call the original function with the provided arguments | |
// This context is necessary for the context of the history change | |
return defaultPushState.apply(this, [data, title, url]); | |
}; | |
}; | |
history.pushState = this.pushState(); | |
} | |
} | |
/** | |
* New page reload handler | |
*/ | |
private handleNewPageReload(count: number = 0) { | |
this.isNewPage = false; | |
const crntHref = location.href.toLowerCase(); | |
console.log("handleNewPageReload:", crntHref); | |
if (crntHref.indexOf("newpage.aspx") !== -1 || crntHref.indexOf("mode=edit") !== -1) { | |
if (count < 5) { | |
setTimeout(() => { | |
this.handleNewPageReload(++count); | |
}, 50); | |
} | |
} else { | |
// The page is ready to be reloaded | |
location.reload(); | |
} | |
} |
The bindToHistory method will need to be called from the application customizer its onInit method. Inside the method, we bind into the history API, so every time a page navigation change happens, this will be seen by the binding. On each change, the new URL gets validated if it contains newpage.aspx and if it is in edit mode. That way we know that it is a new page.
Whenever you click the save or publish button, there could be two scenarios:
The page was not automatically saved, which means newpage.aspx is still included in the URL; Page was already saved, the URL doesn’t contain newpage.aspx anymore, but will have the new page name instead.
Both of these transitions will be captured and will call the handleNewPageReload
method. Inside this method, two checks are happening.
Does the URL still contain newpage.aspx, if that is the case, we have to wait until it has been updated by SharePoint with the new page name. Sometimes it happens that the page transition still includes the edit mode in the URL. In most cases, this happens when the page was automatically saved by SharePoint. In this case, you have to wait until the page is back in reading mode.
Due to the above checks, we need to wait and that is why the setTimeout
is implemented and it will only check it 5 times. In my environment(s), after the second check, it is already fine.
Once the page shows the read/view mode, the page will be fully reloaded. The reason for this is because we need to update the context of the application customizer. Otherwise, it keeps using the context of the previous page.
Once this code has been added in your application customizer, you will see the following behaviour:

The full code with the previous checks in place looks like this:
import { override } from '@microsoft/decorators'; | |
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base'; | |
import { SPEventArgs } from '@microsoft/sp-core-library'; | |
interface NavigationEventDetails extends Window { | |
isNavigatedEventSubscribed: boolean; | |
currentPage: string; | |
currentHubSiteId: string; | |
currentUICultureName: string; | |
} | |
declare const window: NavigationEventDetails; | |
/** A Custom Action which can be run during execution of a Client Side Application */ | |
export default class NavigationEventApplicationCustomizer extends BaseApplicationCustomizer<{}> { | |
private pushState: () => any = null; | |
private isNewPage: boolean = false; | |
@override | |
public async onInit(): Promise<void> { | |
console.log("onInit called:", window.location.pathname); | |
// Bind into the browser history | |
this.bindToHistory(); | |
this.render(); | |
} | |
@override | |
public async onDispose(): Promise<void> { | |
this.context.application.navigatedEvent.remove(this, this.render); | |
window.isNavigatedEventSubscribed = false; | |
window.currentPage = ''; | |
window.currentHubSiteId = ''; | |
window.currentUICultureName = ''; | |
} | |
private async render() { | |
console.log("NavigationEventApplicationCustomizer render", window.location.pathname); | |
window.currentPage = window.location.href; | |
window.currentHubSiteId = HubSiteService.getHubSiteId(); | |
window.currentUICultureName = this.context.pageContext.cultureInfo.currentUICultureName; | |
if (!window.isNavigatedEventSubscribed) { | |
window.isNavigatedEventSubscribed = true; | |
this.context.application.navigatedEvent.add(this, this.navigationEventHandler); | |
} | |
} | |
private navigationEventHandler(args: SPEventArgs): void { | |
console.log("NavigationEventHandler called:", window.location.pathname); | |
setTimeout(() => { | |
if (window.currentHubSiteId !== HubSiteService.getHubSiteId()) { | |
this.onDispose(); | |
this.onInit(); | |
return; | |
} | |
if (window.currentUICultureName !== this.context.pageContext.cultureInfo.currentUICultureName) { | |
// Trigger a full page refresh to be sure to have to correct language loaded | |
location.reload(); | |
return; | |
} | |
if (window.currentPage !== window.location.href) { | |
console.log("NavigationEventHandler: Trigger render again, as page was cached"); | |
this.render(); | |
} | |
}, 50); | |
} | |
/** | |
* Bind the control to the browser history to do metadata check when page is in edit mode | |
*/ | |
private bindToHistory(): void { | |
// Only need to bind the pushState once to the history | |
if (!this.pushState) { | |
// Binding to page mode changes | |
this.pushState = () => { | |
const defaultPushState = history.pushState; | |
// We need the current this context to update the component its state | |
const self = this; | |
return function (data: any, title: string, url?: string | null) { | |
console.log("bindToHistory:", url); | |
// Check if you navigated to a new page creation | |
if (url.indexOf("newpage.aspx")) { | |
// Check if the page is in edit mode | |
if (url.toLowerCase().indexOf('mode=edit') !== -1) { | |
self.isNewPage = true; | |
} else if (self.isNewPage) { | |
// Page is created | |
self.handleNewPageReload(); | |
} | |
} else if (self.isNewPage) { | |
// Page is created | |
self.handleNewPageReload(); | |
} | |
// Call the original function with the provided arguments | |
// This context is necessary for the context of the history change | |
return defaultPushState.apply(this, [data, title, url]); | |
}; | |
}; | |
history.pushState = this.pushState(); | |
} | |
} | |
/** | |
* New page reload handler | |
*/ | |
private handleNewPageReload(count: number = 0) { | |
this.isNewPage = false; | |
const crntHref = location.href.toLowerCase(); | |
console.log("handleNewPageReload:", crntHref); | |
if (crntHref.indexOf("newpage.aspx") !== -1 || crntHref.indexOf("mode=edit") !== -1) { | |
if (count < 5) { | |
setTimeout(() => { | |
this.handleNewPageReload(++count); | |
}, 50); | |
} | |
} else { | |
// The page is ready to be reloaded | |
location.reload(); | |
} | |
} | |
} |
Happy coding
Related articles
Aborting your fetch requests in your SharePoint Framework extensions during navigation events
Showing a spinner when dynamically loading resources for your SPFx property pane
Developing SharePoint Framework solutions outside the workbench
Report issues or make changes on GitHub
Found a typo or issue in this article? Visit the GitHub repository to make changes or submit a bug report.
Comments
Let's build together
Manage content in VS Code
Present from VS Code