PnPjs Configuration in SPFx — The Foundation Before Everything Else
const sp = getSP();
const sp = getSP();
const sp = getSP();
const sp = getSP();
- your
.web.lists.getByTitle()calls fail - authentication context is missing
- SPFx does not know where to execute requests
const sp = getSP();
Why PnPjs Config Matters
SharePoint Framework gives you context.
PnPjs uses that context.
The connection happens here:
SPFx(context)
That tells PnPjs:
- current site URL
- current web URL
- authentication token
- tenant context
- user context
Think of it like dependency injection for SharePoint.
Install PnPjs
Inside your SPFx project:
npm install @pnp/sp --savenpm install @pnp/logging --savenpm install @pnp/queryable --save
Create a Config File
Create:
New-Item -Path ".\src\pnpjsConfig.ts" -ItemType File -Force
This file centralizes all configuration.
pnpjsConfig.ts
import { spfi, SPFI } from "@pnp/sp";import { SPFx } from "@pnp/sp/behaviors/spfx";let _sp: SPFI;export const getSP = (context?: any): SPFI => { if (!_sp && context) { _sp = spfi().using(SPFx(context)); } return _sp;};
Code Breakdown
1. SPFI
import { spfi, SPFI } from "@pnp/sp";
SPFI is the core PnPjs instance.
It becomes your API client.
Example:
const sp = getSP();
Then:
const lists = await sp.web.lists();
2. SPFx Behavior
import { SPFx } from "@pnp/sp/behaviors/spfx";
This injects SPFx context.
Without this:
spfi()
is just an empty shell.
With this:
spfi().using(SPFx(context))
it becomes authenticated.
3. Singleton Pattern
let _sp: SPFI;
Important.
You do not want multiple PnPjs instances.
Why?
Because:
- better performance
- reused configuration
- reused pipeline
- cleaner architecture
This makes _sp a singleton.
4. Lazy Initialization
if (!_sp && context)
Only initializes once.
First render:
getSP(this.context)
After that:
getSP()
works anywhere.
Using It in WebPart
Example:
SpPnPbasicoWebPart.ts
import * as React from 'react';import * as ReactDom from 'react-dom';import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';import SpPnPbasico from './components/SpPnPbasico';import { getSP } from './pnpjsConfig';export default class SpPnPbasicoWebPart extends BaseClientSideWebPart<{}> { public render(): void { getSP(this.context); const element = React.createElement( SpPnPbasico ); ReactDom.render(element, this.domElement); }}
Critical line:
getSP(this.context);
This initializes PnPjs.
Only once.
Using It in React Component
SpPnPbasico.tsx
import * as React from 'react';import { getSP } from '../pnpjsConfig';const SpPnPbasico: React.FC = () => { const loadLists = async () => { const sp = getSP(); const lists = await sp.web.lists(); console.log(lists); }; React.useEffect(() => { loadLists(); }, []); return ( <div> PnPjs Connected </div> );};export default SpPnPbasico;
Execution Flow
SPFx WebPart loads ↓getSP(this.context) ↓PnPjs receives SPFx context ↓React component calls getSP() ↓PnPjs instance reused ↓SharePoint data loaded
Best Practices
Keep one config file
Good:
src/pnpjsConfig.ts
Bad:
multiple duplicated configs
Initialize in WebPart, not component
Correct:
getSP(this.context)
inside:
render()
This guarantees context exists.
Reuse everywhere
Example:
Services:
const sp = getSP();
Hooks:
const sp = getSP();
Components:
const sp = getSP();
Real-world next step
Now you are ready for:
- PnPjs CRUD
- PnP Controls ListView
- DynamicForm
- PeoplePicker
- ListItemPicker
- TaxonomyPicker
- file uploads
- batch operations
This config becomes the backbone of all of them.
Especially your next article:
PnPjs + ListView integration
That’s the natural evolution.
Official Documentation
Final Takeaway
If SPFx is the engine, PnPjs is the transmission.
And this config file is the ignition key.
Simple.
Small.
Essential.
Everything else depends on it.
