
In Part 1 of our series we created a declarative list to hold the placeholder content for our site. In Part 2 we utilized an Application Customizer extension to grab that content and place it into the available placeholders. In this third part, we’ll modify our data model to cache those results in localStorage, to avoid unnecessary trips back to the list in order to grab the content. While I’ve chosen to cache items for 5 minutes, you may want to go with more or less time, depending on your specific needs.
Series Recap:
- Part 1 – Create the Content Repository for managing your Placeholder content
- Part 2 – Inject your managed Placeholder content into your pages
- Part 3 – Cache placeholder content in localStorage
- Part 4 (New!) – Update code for the 1.2.0 SharePoint Framework Extensions Release Candidate
In order to add caching in localStorage, you’ll want to replace your PlaceholderItem.ts contents with the following code:
import * as pnp from 'sp-pnp-js'; import { Environment, EnvironmentType } from '@microsoft/sp-core-library'; const LOCALSTORAGE_PREFIX: string = "SPFXPlaceholders_"; const PLACEHOLDERS_LISTNAME: string = "SPFx Placeholders"; const FIELD_TITLENAME: string = "Title"; const FIELD_SPFXCONTENTNAME: string = "SPFxContent"; const CACHE_EXPIRATIONMINUTES: number = 5; export interface IPlaceholderItem { Title: string; SPFxContent: string; } export interface IPlaceholderCache { Expiration: Number; Content: IPlaceholderItem[]; } export class PlaceholderItems { public static GetItems(webID: string): Promise<IPlaceholderItem[]> { return (Environment.type === EnvironmentType.Local) ? this.GetMockListItems() : this.GetRealListItems(webID); } private static GetRealListItems(webID: string): Promise<IPlaceholderItem[]> { return new Promise<IPlaceholderItem[]>((resolve) => { //Find data in localStorage or retrieve from list const cachedPlaceholders: IPlaceholderCache = localStorage ? JSON.parse(localStorage.getItem(LOCALSTORAGE_PREFIX + webID)) : null; if (cachedPlaceholders && cachedPlaceholders.Expiration > new Date().getTime()) { //return localStorage when available and within cached timeframe resolve(cachedPlaceholders.Content); } else { //return data from list when localStorage unavailable or stale pnp.sp.web.lists.getByTitle(PLACEHOLDERS_LISTNAME).items. select(FIELD_TITLENAME, FIELD_SPFXCONTENTNAME).get().then((data: IPlaceholderItem[]) => { //Save in localStorage if available if (localStorage) { localStorage.setItem(LOCALSTORAGE_PREFIX + webID, JSON.stringify({ Expiration: new Date().getTime() + (CACHE_EXPIRATIONMINUTES * 60 * 1000), Content: data })); } console.log('got it from the list'); resolve(data); }); } }); } private static GetMockListItems(): Promise<IPlaceholderItem[]> { return new Promise<IPlaceholderItem[]>((resolve) => { resolve( [ { Title: "PageHeader", SPFxContent: "Header Content" }, { Title: "PageFooter", SPFxContent: "Footer Content" } ] ); }); } }
Let’s dive into the changes a bit, to see where the caching comes into play.
- We’ve got a couple new variables at the top of the script to help us. The first is a value to prefix our content in localStorage. The second is a number (in minutes) that tells us how long to cache the list information. Again, feel free to change this value if something smaller or larger better suits your needs.
const LOCALSTORAGE_PREFIX: string = "SPFXPlaceholders_"; const CACHE_EXPIRATIONMINUTES: number = 5;
- Next, before we go to the list to grab the placeholder content, we first look to see if it exists in localStorage. However, we’ve added another parameter to our “GetRealListItems” method in order to ensure we can have different placeholder content for different sites within our tenant. This is due to localStorage being tied back to the domain, similar to the way cookies are. Without appending the web id of a given site onto our content key, all sites would read the same placeholder content in localStorge, and would also overwrite each other when caching. Appending our web id allows us to keep placeholder content unique to a given site. We check for browser support of localStorage, and if available, we try and grab our content and parse it into JSON. If content is available, and if it hasn’t yet expired (we store the expiration date when we cache it), then we’ll simply return the cached data back.
private static GetRealListItems(webID: string): Promise<IPlaceholderItem[]> { return new Promise<IPlaceholderItem[]>((resolve) => { //Find data in localStorage or retrieve from list const cachedPlaceholders: IPlaceholderCache = localStorage ? JSON.parse(localStorage.getItem(LOCALSTORAGE_PREFIX + webID)) : null; if (cachedPlaceholders && cachedPlaceholders.Expiration > new Date().getTime()) { //return localStorage when available and within cached timeframe resolve(cachedPlaceholders.Content); } else { //...
- The last change is caching the data after we grab it from the list (which we refer to in the above step). We again first check for localStorage support, and when available, we store a string-representation of a JSON object that contains our list content, as well as a new expiration date that is calculated as the current time plus the number of expiration minutes we have declared at the top of our script.
//return data from list when localStorage unavailable or stale pnp.sp.web.lists.getByTitle(PLACEHOLDERS_LISTNAME).items. select(FIELD_TITLENAME, FIELD_SPFXCONTENTNAME).get().then((data: IPlaceholderItem[]) => { //Save in localStorage if available if (localStorage) { localStorage.setItem(LOCALSTORAGE_PREFIX + webID, JSON.stringify({ Expiration: new Date().getTime() + (CACHE_EXPIRATIONMINUTES * 60 * 1000), Content: data })); } resolve(data); });
After making these changes, we’re now ready to not only continue allowing our editors to easily manage this placeholder content for current and new placeholders available in the future, but we’re also caching this data to improve the experience for our end users.
Cheers,
Matt