Building Modern SharePoint Solutions with SPFx, PnPjs and PnP ListView

From Raw Data to Professional UI

When building enterprise-grade SharePoint solutions with SPFx, one of the most common mistakes developers make is jumping directly into UI controls without understanding the data layer first.

This usually creates:

  • tightly coupled code
  • duplicated logic
  • difficult maintenance
  • poor scalability

A much better approach is to separate:

Data Layer
and

Presentation Layer

This article introduces that architecture using:

  • SPFx
  • PnPjs
  • PnP React Controls
  • ListView

This is one of the most reusable patterns you can learn.


The architecture

The flow:

SharePoint List
PnPjs
Service / Component Logic
React State
PnP ListView
User Interface

This separation is important.

PnPjs retrieves.

ListView displays.

React controls the state.

This is clean architecture.


Why PnPjs first?

Before touching UI, we must understand how data is retrieved.

PnPjs is the official community-driven abstraction layer for SharePoint REST.

Official docs:

PnPjs Getting Started:

PnPjs SharePoint docs:

PnP React Controls docs:

ListView docs:

These are essential references.


Part 1 — Setting up PnPjs

Install:

npm install @pnp/sp

Why centralize configuration?

A common beginner mistake:

const sp = spfi().using(SPFx(context));

inside every component.

Bad idea.

This creates multiple instances.

Better:

single reusable instance.


pnpjsConfig.ts

Create:

src/webparts/spPnPbasico/components/pnpjsConfig.ts

Code:

import { spfi, SPFI } from "@pnp/sp";
import { SPFx } from "@pnp/sp/behaviors/spfx";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
let _sp: SPFI | undefined;
export const getSP = (context?: any): SPFI => {
if (!_sp && context) {
_sp = spfi().using(SPFx(context));
}
if (!_sp) {
throw new Error("PnPjs has not been initialized.");
}
return _sp;
};

Understanding the imports

These imports are critical:

import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";

Without them:

sp.web
sp.web.lists
sp.web.lists.items

may not exist.

PnPjs uses modular imports.

This is important for bundle optimization.


Passing context from WebPart

Your WebPart passes the SPFx context:

const element: React.ReactElement<ISpPnPbasicoProps> =
React.createElement(
SpPnPbasico,
{
context: this.context
}
);

This is how authentication flows into PnPjs.

Without it, requests fail.


Part 2 — Retrieving SharePoint data

Now let’s create the first data retrieval layer.

This stage is intentionally simple.

No UI.

Only retrieval.


First component

import * as React from 'react';
import { useEffect } from "react";
import { ISpPnPbasicoProps } from './ISpPnPbasicoProps';
import { getSP } from "./pnpjsConfig";
const SpPnPbasico: React.FC<ISpPnPbasicoProps> = (props) => {
useEffect(() => {
loadItems();
}, []);
const loadItems = async (): Promise<void> => {
try {
const sp = getSP(props.context);
const items = await sp.web.lists
.getByTitle("Bike Sales")
.items
.select("Id", "Title")();
console.log("Bike Sales items:", items);
} catch (error) {
console.error("Error loading items:", error);
}
};
return (
<div>
<h1>Welcome to SPFx with PnPjs!</h1>
<p>Check the browser console to see the list items.</p>
</div>
);
};
export default SpPnPbasico;

Breaking down the retrieval

useEffect

useEffect(() => {
loadItems();
}, []);

Runs once.

Equivalent to componentDidMount.


getSP

const sp = getSP(props.context);

Gets the singleton instance.


getByTitle

.getByTitle("Bike Sales")

Targets the list.


items

.items

Targets the collection.


select

.select("Id", "Title")

Very important.

Always reduce payload.

Good performance practice.


Why this matters

At this point you already know:

  • connect to SharePoint
  • authenticate
  • query lists
  • optimize fields

This is the foundation.

Now comes UI.


Part 3 — Evolving to PnP ListView

Now we stop using:

console.log(items)

and render data.

This is the natural evolution.

Install:

npm install @pnp/spfx-controls-react

Why ListView?

PnP ListView is a powerful wrapper over Fluent UI.

Benefits:

  • sorting
  • filtering
  • selection
  • responsive layout
  • custom rendering
  • consistent UI

Instead of manually building HTML tables.

Much faster.

Much cleaner.


The evolution

Old:

PnPjs → Console

New:

PnPjs → React State → ListView

Huge evolution.


Full ListView component

import * as React from 'react';
import { useEffect, useState } from "react";
import { ISpPnPbasicoProps } from './ISpPnPbasicoProps';
import { getSP } from "./pnpjsConfig";
import {
ListView,
IViewField,
SelectionMode
} from "@pnp/spfx-controls-react/lib/ListView";
interface IBikeSaleItem {
Id: number;
Title: string;
}
const SpPnPbasico: React.FC<ISpPnPbasicoProps> = (props) => {
const [items, setItems] = useState<IBikeSaleItem[]>([]);
useEffect(() => {
loadItems();
}, []);
const loadItems = async (): Promise<void> => {
try {
const sp = getSP(props.context);
const listItems: IBikeSaleItem[] = await sp.web.lists
.getByTitle("Bike Sales")
.items
.select("Id", "Title")();
setItems(listItems);
} catch (error) {
console.error("Error loading Bike Sales items:", error);
}
};
const viewFields: IViewField[] = [
{
name: "Id",
displayName: "ID",
sorting: true,
minWidth: 50,
maxWidth: 80
},
{
name: "Title",
displayName: "Title",
sorting: true,
minWidth: 150
}
];
return (
<div>
<h1>Bike Sales</h1>
<p>SharePoint list data rendered with PnP ListView.</p>
<ListView
items={items}
viewFields={viewFields}
compact={false}
selectionMode={SelectionMode.none}
showFilter={true}
filterPlaceHolder="Search Bike Sales..."
/>
</div>
);
};
export default SpPnPbasico;

What changed?

Before:

console.log(items);

Now:

setItems(listItems);

React stores state.

Then:

<ListView items={items} />

renders UI.

This is the bridge.


Why this pattern scales

This same architecture works for:

  • Document Libraries
  • Search Results
  • Approval Lists
  • Training Catalogs
  • Dashboard Widgets
  • Audit Reports
  • Purview exports
  • Power BI feeders

You can reuse this pattern everywhere.


Next evolution

Now that we have:

PnPjs
+
ListView

the next logical controls are:

  • ListItemPicker
  • PeoplePicker
  • TaxonomyPicker
  • FilePicker
  • FolderPicker
  • RichText
  • DynamicForm

This is where SPFx becomes powerful.

But it all starts here.

PnPjs first.

UI second.

That order matters.


Final architecture

Best practice:

WebPart
Context
PnPjs Config
Service Layer
Component
State
PnP ListView

This is the foundation for modern SharePoint development.

Master this.

Everything else builds on top of it.

Edvaldo Guimrães Filho Avatar

Published by