
Série SPFx : Comprendre les Props et l'État
Props et état, qu'est-ce que c'est et à quoi ça sert ?
Si vous développez des solutions SPFx depuis un moment, vous avez probablement entendu parler de PnPjs. Et si vous n’avez pas encore commencé à l’utiliser, vous vous compliquez la vie inutilement. PnPjs est une collection open-source de bibliothèques Fluent qui encapsule l’API REST SharePoint et des parties de l’API Microsoft Graph dans des appels propres, chaînables et typés. Fini de se battre avec des requêtes fetch brutes ou de construire manuellement des chaînes de requêtes.
Dans cet article, je vais vous guider à travers tout ce que vous devez savoir pour commencer : quelle version utiliser, comment la configurer, et les trois principales façons de structurer votre code.
Avant d’écrire une seule ligne de code, vous devez choisir la bonne version. Si vous vous trompez, vous perdrez du temps à chercher des erreurs bizarres.
La version 2 est ce qu’il vous faut si vous exécutez SharePoint on-premises (2016 ou 2019), ou si votre version SPFx est antérieure à 1.12.1. Pour tout ce qui est moderne et basé sur le cloud, regardez la v3 ou la v4.
La version 3 prend en charge SPFx 1.12.1 à 1.17.4. Si vous êtes sur 1.12.1 à 1.14.0, des étapes de configuration supplémentaires sont nécessaires pour faire fonctionner TypeScript 4.x. Les instructions complètes se trouvent sur pnp.github.io/pnpjs/getting-started. À partir de 1.15.0, la v3 fonctionne directement.
La version 4 est la version la plus récente et c’est celle pour laquelle cet article est écrit. Elle nécessite Node.js 18 et ne fonctionne donc qu’avec SPFx 1.18.0 et versions ultérieures. La bonne nouvelle : l’API est essentiellement identique à la v3, donc la mise à niveau est simple.
Deux packages couvrent la plupart des cas d’utilisation :
npm install @pnp/sp @pnp/graph --save
@pnp/sp vous donne accès à l’API REST SharePoint, et @pnp/graph couvre l’API Microsoft Graph. Pendant que vous y êtes, ajoutez également le package de journalisation — vous vous en féliciterez pendant le développement :
npm install @pnp/logging --save
Le plus grand changement conceptuel de PnPjs v2 à v3/v4 est la façon dont vous configurez le contexte. Dans la v2, il y avait un appel global sp.setup() que vous faisiez une fois et oubliiez. Ce modèle a disparu.
Dans v3/v4, vous créez des instances à l’aide d’interfaces de fabrique : spfi pour SharePoint et graphfi pour Microsoft Graph. Les deux ont besoin de l’objet de contexte SPFx pour savoir qui vous êtes, dans quel tenant vous vous trouvez et comment authentifier les requêtes.
Il existe trois façons de structurer cela dans votre projet. Je vais vous les présenter toutes les trois.
L’approche la plus simple. Définissez votre instance spfi directement dans onInit() et utilisez-la à partir de là. Cela fonctionne bien pour les petits WebParts autonomes où vous n’avez pas beaucoup de fichiers faisant des appels API.
import { spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
export default class HelloWorldWebPart extends BaseClientSideWebPart {
protected async onInit(): Promise {
await super.onInit();
const sp = spfi().using(SPFx(this.context));
// Utilisez sp directement ici, ou transmettez-le à votre composant via des props
const lists = await sp.web.lists();
console.log(lists);
}
}
Si vous avez besoin à la fois de SharePoint et de Graph dans le même projet, vous devez aliaser l’import SPFx. @pnp/sp et @pnp/graph exportent tous les deux un comportement avec ce nom exact, donc sans alias, le compilateur TypeScript se plaindra :
import { spfi, SPFx as spSPFx } from "@pnp/sp";
import { graphfi, SPFx as graphSPFx } from "@pnp/graph";
protected async onInit(): Promise {
await super.onInit();
const sp = spfi().using(spSPFx(this.context));
const graph = graphfi().using(graphSPFx(this.context));
}
Cette approche est rapide à configurer, mais elle devient désordonnée dès que vous commencez à passer l’objet sp à travers plusieurs couches de composants React via des props. Pour tout ce qui est plus grand qu’un WebPart à fichier unique, utilisez l’option 2 ou 3.
C’est ma préférence pour la plupart des projets. Vous créez un fichier central pnpjs-config.ts dans votre dossier src qui gère toute la configuration et exporte une fonction getSP(). N’importe quel fichier dans votre projet qui doit faire des appels API importe et appelle simplement cette fonction — pas besoin de passer le contexte partout, pas de prop drilling.
Étape 1 : Créez src/pnpjs-config.ts
import { WebPartContext } from "@microsoft/sp-webpart-base";
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";
var _sp: SPFI | null = null;
export const getSP = (context?: WebPartContext): SPFI => {
if (context != null) {
_sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));
}
return _sp;
};
Quelques éléments à noter ici. Les imports sélectifs, @pnp/sp/webs, @pnp/sp/lists, etc., sont très importants en v3/v4. PnPjs utilise le tree shaking, donc seul ce que vous importez explicitement est inclus dans le bundle. N’importez pas tout aveuglément. Importez uniquement ce dont vous avez besoin et gardez votre bundle léger.
Le comportement PnPLogging ajoute la journalisation des appels API dans la console du navigateur. Utile pendant le développement, mais réduisez le niveau ou supprimez-le entièrement avant de passer en production.
Étape 2 : Initialisez depuis onInit dans votre WebPart
// HelloWorldWebPart.ts
import { getSP } from "./pnpjs-config";
import { SPFI } from "@pnp/sp";
export default class HelloWorldWebPart extends BaseClientSideWebPart {
private _sp: SPFI;
protected async onInit(): Promise {
await super.onInit();
this._sp = getSP(this.context); // Initialisez une fois avec le contexte
}
public render(): void {
// ... votre code de rendu
}
}
Le modèle est simple : le premier appel à getSP(this.context) crée et stocke l’instance. Chaque appel ultérieur à getSP() sans arguments retourne cette même instance stockée.
Si vous avez également besoin de Graph, étendez le fichier de configuration avec une fonction getGraph(). En raison du conflit de noms mentionné précédemment, vous devrez aliaser les imports SPFx :
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { spfi, SPFI, SPFx as spSPFx } from "@pnp/sp";
import { graphfi, GraphFI, SPFx as graphSPFx } from "@pnp/graph";
import { LogLevel, PnPLogging } from "@pnp/logging";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/batching";
var _sp: SPFI | null = null;
var _graph: GraphFI | null = null;
export const getSP = (context?: WebPartContext): SPFI => {
if (context != null) {
_sp = spfi().using(spSPFx(context)).using(PnPLogging(LogLevel.Warning));
}
return _sp;
};
export const getGraph = (context?: WebPartContext): GraphFI => {
if (context != null) {
_graph = graphfi().using(graphSPFx(context)).using(PnPLogging(LogLevel.Warning));
}
return _graph;
};
Puis initialisez les deux dans onInit :
protected async onInit(): Promise {
await super.onInit();
getSP(this.context);
getGraph(this.context);
}
Option 3 : Utiliser une classe de service
Pour les projets plus grands et plus complexes où vous souhaitez une injection de dépendances appropriée et une séparation claire entre votre couche API et votre couche UI, une classe de service est le bon modèle.
La principale différence ici est qu’un service n’a pas accès direct à this.context depuis le WebPart. Au lieu de cela, il fonctionne avec ServiceScope, le conteneur d’injection de dépendances SPFx, et consomme PageContext et AadTokenProviderFactory à partir de celui-ci.
La classe de service :
// services/SampleService.ts
import { ServiceKey, ServiceScope } from "@microsoft/sp-core-library";
import { PageContext } from "@microsoft/sp-page-context";
import { AadTokenProviderFactory } from "@microsoft/sp-http";
import { spfi, SPFI, SPFx as spSPFx } from "@pnp/sp";
import { graphfi, GraphFI, SPFx as gSPFx } from "@pnp/graph";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
export interface ISampleService {
getLists(): Promise;
}
export class SampleService implements ISampleService {
public static readonly serviceKey: ServiceKey =
ServiceKey.create("SPFx:SampleService", SampleService);
private _sp: SPFI;
private _graph: GraphFI;
constructor(serviceScope: ServiceScope) {
serviceScope.whenFinished(() => {
const pageContext = serviceScope.consume(PageContext.serviceKey);
const aadTokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey);
this._sp = spfi().using(spSPFx({ pageContext }));
this._graph = graphfi().using(gSPFx({ aadTokenProviderFactory }));
});
}
public getLists(): Promise {
return this._sp.web.lists();
}
}
Utilisation dans votre WebPart :
// HelloWorldWebPart.ts
import { SampleService, ISampleService } from "./services/SampleService";
export default class HelloWorldWebPart extends BaseClientSideWebPart {
private _sampleService: ISampleService;
protected async onInit(): Promise {
await super.onInit();
this._sampleService = this.context.serviceScope.consume(SampleService.serviceKey);
}
public render(): void {
// Transmettez _sampleService à votre composant via des props
}
}
Voici une façon simple d’y réfléchir :