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 --save
npm install @pnp/logging --save
npm 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

PnPjs Official Documentation

SPFx 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.

Edvaldo Guimrães Filho Avatar

Published by