5 minutes read
If you are building SharePoint Framework (SPFx) solutions, PnPjs is likely your best friend. It drastically simplifies the complex SharePoint REST API into a fluent, readable, and type-safe JavaScript library.
However, recent versions (v3 and v4) introduced a major shift in configuration—moving away from the global sp object to a more modular SPFI (SharePoint Factory Interface) pattern. If you are still using sp.setup(), it’s time to upgrade.
In this guide, we will cover the latest configuration steps and perform full CRUD operations in an SPFx Web Part.
Step 1: Installation
First, install the necessary packages. In the modern PnPjs architecture, packages are modular. We need the core sp package for SharePoint operations and logging for debugging.
Run this command in your SPFx project terminal:
Bash
npm install @pnp/sp @pnp/logging --save
Note: If you are using SPFx v1.18 or later, you are likely on Node.js v18, which supports PnPjs v4. If you are on an older SPFx version, ensure you check compatibility, as PnPjs v4 requires Node v16+.
Step 2: Centralized Configuration (The Modern Way)
The “latest” best practice is to avoid configuring PnPjs inside your web part file directly. Instead, create a centralized configuration file. This prevents “multiple instance” errors and makes your code cleaner.
Create a new file named
pnpjsConfig.tsinside yoursrc/webparts/{webPartName}/folder.Paste the following code:
TypeScript
import { WebPartContext } from "@microsoft/sp-webpart-base";
// import pnp, pnp logging system, and any other selective imports needed
import { spfi, SPFI, SPFx } from "@pnp/sp";
import { LogLevel, PnPLogging } from "@pnp/logging";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/batching";
// A variable to hold the active instance
let _sp: SPFI = null;
export const getSP = (context?: WebPartContext): SPFI => {
if (_sp === null && context !== null) {
// Initialize the instance with SPFx context
_sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));
}
return _sp;
};
Why this approach?
Singleton Pattern: It ensures
_spis initialized only once.Selective Imports: We only import what we need (
webs,lists,items), which keeps the bundle size small (Tree Shaking).
Step 3: Initializing in Your Web Part
Now, go to your main Web Part file (e.g., HelloWorldWebPart.ts). You need to initialize the library in the onInit method.
TypeScript
import { getSP } from './pnpjsConfig';
import { SPFI } from '@pnp/sp';
export default class HelloWorldWebPart extends BaseClientSideWebPart<IHelloWorldWebPartProps> {
private _sp: SPFI;
protected async onInit(): Promise<void> {
await super.onInit();
// Initialize our _sp object that we can then use in other packages without having to pass around the context.
this._sp = getSP(this.context);
}
public render(): void {
// ... your render code
}
}
Step 4: Performing CRUD Operations
Now that configuration is out of the way, let’s look at the code for Create, Read, Update, and Delete.
1. Create (Add a New Item)
To add an item, we access the list and call .add().
TypeScript
const createItem = async () => {
try {
const list = this._sp.web.lists.getByTitle("MyList");
const addItemResult = await list.items.add({
Title: "New Project Proposal",
Description: "Created via PnPjs in SPFx",
Status: "Draft" // Assuming you have a 'Status' choice column
});
console.log(`Item created successfully with ID: ${addItemResult.data.Id}`);
} catch (e) {
console.error(e);
}
};
2. Read (Get Items)
Fetching items often requires filtering and selecting specific columns to optimize performance.
TypeScript
const getItems = async () => {
try {
// 'select' limits the columns returned (Best Practice)
// 'filter' gets only specific items
const items = await this._sp.web.lists.getByTitle("MyList").items
.select("Id", "Title", "Description", "Created")
.filter("Status eq 'Draft'")
.orderBy("Created", false)();
console.log(items);
return items;
} catch (e) {
console.error(e);
}
};
3. Update (Edit an Item)
To update, you need the item’s ID. Note that update() performs a merge (only fields you pass are changed).
TypeScript
const updateItem = async (itemId: number) => {
try {
const list = this._sp.web.lists.getByTitle("MyList");
await list.items.getById(itemId).update({
Title: "Updated Project Proposal",
Status: "Submitted"
});
console.log("Item updated successfully!");
} catch (e) {
console.error(e);
}
};
4. Delete (Remove an Item)
Deleting is straightforward but permanent. Be careful!
TypeScript
const deleteItem = async (itemId: number) => {
try {
const list = this._sp.web.lists.getByTitle("MyList");
await list.items.getById(itemId).delete();
console.log("Item deleted successfully!");
} catch (e) {
console.error(e);
}
};
Step 5: Putting it together (React Example)
If you are using React, pass the _sp instance down to your components via props.
In your WebPart.ts:
TypeScript
const element: React.ReactElement<IHelloWorldProps> = React.createElement(
HelloWorld,
{
sp: this._sp // Pass the configured instance
}
);
In your React Component:
TypeScript
export interface IHelloWorldProps {
sp: SPFI;
}
export default class HelloWorld extends React.Component<IHelloWorldProps, {}> {
componentDidMount() {
this.readItems();
}
private readItems = async () => {
const items = await this.props.sp.web.lists.getByTitle("MyList").items();
console.log(items);
}
// ... render method
}
Conclusion
Using the latest PnPjs (v3/v4) requires a slight shift in mindset from the “global object” days, but the result is a faster, safer, and more modular application.
Key Takeaways:
Centralize Config: Use a
pnpjsConfig.tsfile.Use
spfi: Thespfi().using(SPFx(context))syntax is the new standard.Select imports: Only import the sub-libraries (like
@pnp/sp/webs) that you actually use.
Happy Coding! 🚀