@apvee/spfx-react-toolkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2012 -0
- package/lib/core/atoms.internal.d.ts +53 -0
- package/lib/core/atoms.internal.d.ts.map +1 -0
- package/lib/core/atoms.internal.js +35 -0
- package/lib/core/atoms.internal.js.map +1 -0
- package/lib/core/context.internal.d.ts +23 -0
- package/lib/core/context.internal.d.ts.map +1 -0
- package/lib/core/context.internal.js +34 -0
- package/lib/core/context.internal.js.map +1 -0
- package/lib/core/index.d.ts +6 -0
- package/lib/core/index.d.ts.map +1 -0
- package/lib/core/index.js +6 -0
- package/lib/core/index.js.map +1 -0
- package/lib/core/provider-application-customizer.d.ts +57 -0
- package/lib/core/provider-application-customizer.d.ts.map +1 -0
- package/lib/core/provider-application-customizer.js +45 -0
- package/lib/core/provider-application-customizer.js.map +1 -0
- package/lib/core/provider-base.internal.d.ts +20 -0
- package/lib/core/provider-base.internal.d.ts.map +1 -0
- package/lib/core/provider-base.internal.js +126 -0
- package/lib/core/provider-base.internal.js.map +1 -0
- package/lib/core/provider-field-customizer.d.ts +58 -0
- package/lib/core/provider-field-customizer.d.ts.map +1 -0
- package/lib/core/provider-field-customizer.js +46 -0
- package/lib/core/provider-field-customizer.js.map +1 -0
- package/lib/core/provider-listview-commandset.d.ts +60 -0
- package/lib/core/provider-listview-commandset.d.ts.map +1 -0
- package/lib/core/provider-listview-commandset.js +48 -0
- package/lib/core/provider-listview-commandset.js.map +1 -0
- package/lib/core/provider-webpart.d.ts +48 -0
- package/lib/core/provider-webpart.d.ts.map +1 -0
- package/lib/core/provider-webpart.js +36 -0
- package/lib/core/provider-webpart.js.map +1 -0
- package/lib/core/types.d.ts +84 -0
- package/lib/core/types.d.ts.map +1 -0
- package/lib/core/types.js +4 -0
- package/lib/core/types.js.map +1 -0
- package/lib/hooks/index.d.ts +34 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +34 -0
- package/lib/hooks/index.js.map +1 -0
- package/lib/hooks/useSPFxAadHttpClient.d.ts +231 -0
- package/lib/hooks/useSPFxAadHttpClient.d.ts.map +1 -0
- package/lib/hooks/useSPFxAadHttpClient.js +299 -0
- package/lib/hooks/useSPFxAadHttpClient.js.map +1 -0
- package/lib/hooks/useSPFxContainerInfo.d.ts +41 -0
- package/lib/hooks/useSPFxContainerInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxContainerInfo.js +47 -0
- package/lib/hooks/useSPFxContainerInfo.js.map +1 -0
- package/lib/hooks/useSPFxContainerSize.d.ts +119 -0
- package/lib/hooks/useSPFxContainerSize.d.ts.map +1 -0
- package/lib/hooks/useSPFxContainerSize.js +150 -0
- package/lib/hooks/useSPFxContainerSize.js.map +1 -0
- package/lib/hooks/useSPFxContext.d.ts +14 -0
- package/lib/hooks/useSPFxContext.d.ts.map +1 -0
- package/lib/hooks/useSPFxContext.js +16 -0
- package/lib/hooks/useSPFxContext.js.map +1 -0
- package/lib/hooks/useSPFxCorrelationInfo.d.ts +51 -0
- package/lib/hooks/useSPFxCorrelationInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxCorrelationInfo.js +58 -0
- package/lib/hooks/useSPFxCorrelationInfo.js.map +1 -0
- package/lib/hooks/useSPFxCrossSitePermissions.d.ts +81 -0
- package/lib/hooks/useSPFxCrossSitePermissions.d.ts.map +1 -0
- package/lib/hooks/useSPFxCrossSitePermissions.js +132 -0
- package/lib/hooks/useSPFxCrossSitePermissions.js.map +1 -0
- package/lib/hooks/useSPFxDisplayMode.d.ts +61 -0
- package/lib/hooks/useSPFxDisplayMode.d.ts.map +1 -0
- package/lib/hooks/useSPFxDisplayMode.js +69 -0
- package/lib/hooks/useSPFxDisplayMode.js.map +1 -0
- package/lib/hooks/useSPFxEnvironmentInfo.d.ts +63 -0
- package/lib/hooks/useSPFxEnvironmentInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxEnvironmentInfo.js +91 -0
- package/lib/hooks/useSPFxEnvironmentInfo.js.map +1 -0
- package/lib/hooks/useSPFxFluent9ThemeInfo.d.ts +105 -0
- package/lib/hooks/useSPFxFluent9ThemeInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxFluent9ThemeInfo.js +136 -0
- package/lib/hooks/useSPFxFluent9ThemeInfo.js.map +1 -0
- package/lib/hooks/useSPFxHubSiteInfo.d.ts +80 -0
- package/lib/hooks/useSPFxHubSiteInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxHubSiteInfo.js +127 -0
- package/lib/hooks/useSPFxHubSiteInfo.js.map +1 -0
- package/lib/hooks/useSPFxInstanceInfo.d.ts +41 -0
- package/lib/hooks/useSPFxInstanceInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxInstanceInfo.js +40 -0
- package/lib/hooks/useSPFxInstanceInfo.js.map +1 -0
- package/lib/hooks/useSPFxListInfo.d.ts +64 -0
- package/lib/hooks/useSPFxListInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxListInfo.js +70 -0
- package/lib/hooks/useSPFxListInfo.js.map +1 -0
- package/lib/hooks/useSPFxLocaleInfo.d.ts +123 -0
- package/lib/hooks/useSPFxLocaleInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxLocaleInfo.js +109 -0
- package/lib/hooks/useSPFxLocaleInfo.js.map +1 -0
- package/lib/hooks/useSPFxLogger.d.ts +108 -0
- package/lib/hooks/useSPFxLogger.d.ts.map +1 -0
- package/lib/hooks/useSPFxLogger.js +117 -0
- package/lib/hooks/useSPFxLogger.js.map +1 -0
- package/lib/hooks/useSPFxMSGraphClient.d.ts +200 -0
- package/lib/hooks/useSPFxMSGraphClient.d.ts.map +1 -0
- package/lib/hooks/useSPFxMSGraphClient.js +264 -0
- package/lib/hooks/useSPFxMSGraphClient.js.map +1 -0
- package/lib/hooks/useSPFxOneDriveAppData.d.ts +264 -0
- package/lib/hooks/useSPFxOneDriveAppData.d.ts.map +1 -0
- package/lib/hooks/useSPFxOneDriveAppData.js +395 -0
- package/lib/hooks/useSPFxOneDriveAppData.js.map +1 -0
- package/lib/hooks/useSPFxPageContext.d.ts +37 -0
- package/lib/hooks/useSPFxPageContext.d.ts.map +1 -0
- package/lib/hooks/useSPFxPageContext.js +49 -0
- package/lib/hooks/useSPFxPageContext.js.map +1 -0
- package/lib/hooks/useSPFxPageType.d.ts +82 -0
- package/lib/hooks/useSPFxPageType.d.ts.map +1 -0
- package/lib/hooks/useSPFxPageType.js +137 -0
- package/lib/hooks/useSPFxPageType.js.map +1 -0
- package/lib/hooks/useSPFxPerformance.d.ts +72 -0
- package/lib/hooks/useSPFxPerformance.d.ts.map +1 -0
- package/lib/hooks/useSPFxPerformance.js +167 -0
- package/lib/hooks/useSPFxPerformance.js.map +1 -0
- package/lib/hooks/useSPFxPermissions.d.ts +61 -0
- package/lib/hooks/useSPFxPermissions.d.ts.map +1 -0
- package/lib/hooks/useSPFxPermissions.js +73 -0
- package/lib/hooks/useSPFxPermissions.js.map +1 -0
- package/lib/hooks/useSPFxPnP.d.ts +539 -0
- package/lib/hooks/useSPFxPnP.d.ts.map +1 -0
- package/lib/hooks/useSPFxPnP.js +533 -0
- package/lib/hooks/useSPFxPnP.js.map +1 -0
- package/lib/hooks/useSPFxPnPContext.d.ts +290 -0
- package/lib/hooks/useSPFxPnPContext.d.ts.map +1 -0
- package/lib/hooks/useSPFxPnPContext.js +340 -0
- package/lib/hooks/useSPFxPnPContext.js.map +1 -0
- package/lib/hooks/useSPFxPnPList.d.ts +545 -0
- package/lib/hooks/useSPFxPnPList.d.ts.map +1 -0
- package/lib/hooks/useSPFxPnPList.js +906 -0
- package/lib/hooks/useSPFxPnPList.js.map +1 -0
- package/lib/hooks/useSPFxPnPSearch.d.ts +540 -0
- package/lib/hooks/useSPFxPnPSearch.d.ts.map +1 -0
- package/lib/hooks/useSPFxPnPSearch.js +672 -0
- package/lib/hooks/useSPFxPnPSearch.js.map +1 -0
- package/lib/hooks/useSPFxProperties.d.ts +80 -0
- package/lib/hooks/useSPFxProperties.d.ts.map +1 -0
- package/lib/hooks/useSPFxProperties.js +95 -0
- package/lib/hooks/useSPFxProperties.js.map +1 -0
- package/lib/hooks/useSPFxSPHttpClient.d.ts +218 -0
- package/lib/hooks/useSPFxSPHttpClient.d.ts.map +1 -0
- package/lib/hooks/useSPFxSPHttpClient.js +287 -0
- package/lib/hooks/useSPFxSPHttpClient.js.map +1 -0
- package/lib/hooks/useSPFxServiceScope.d.ts +107 -0
- package/lib/hooks/useSPFxServiceScope.d.ts.map +1 -0
- package/lib/hooks/useSPFxServiceScope.js +105 -0
- package/lib/hooks/useSPFxServiceScope.js.map +1 -0
- package/lib/hooks/useSPFxSiteInfo.d.ts +116 -0
- package/lib/hooks/useSPFxSiteInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxSiteInfo.js +109 -0
- package/lib/hooks/useSPFxSiteInfo.js.map +1 -0
- package/lib/hooks/useSPFxStorage.d.ts +81 -0
- package/lib/hooks/useSPFxStorage.d.ts.map +1 -0
- package/lib/hooks/useSPFxStorage.js +140 -0
- package/lib/hooks/useSPFxStorage.js.map +1 -0
- package/lib/hooks/useSPFxTeams.d.ts +63 -0
- package/lib/hooks/useSPFxTeams.d.ts.map +1 -0
- package/lib/hooks/useSPFxTeams.js +198 -0
- package/lib/hooks/useSPFxTeams.js.map +1 -0
- package/lib/hooks/useSPFxTenantProperty.d.ts +389 -0
- package/lib/hooks/useSPFxTenantProperty.d.ts.map +1 -0
- package/lib/hooks/useSPFxTenantProperty.js +683 -0
- package/lib/hooks/useSPFxTenantProperty.js.map +1 -0
- package/lib/hooks/useSPFxThemeInfo.d.ts +27 -0
- package/lib/hooks/useSPFxThemeInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxThemeInfo.js +33 -0
- package/lib/hooks/useSPFxThemeInfo.js.map +1 -0
- package/lib/hooks/useSPFxUserInfo.d.ts +47 -0
- package/lib/hooks/useSPFxUserInfo.d.ts.map +1 -0
- package/lib/hooks/useSPFxUserInfo.js +47 -0
- package/lib/hooks/useSPFxUserInfo.js.map +1 -0
- package/lib/hooks/useSPFxUserPhoto.d.ts +270 -0
- package/lib/hooks/useSPFxUserPhoto.d.ts.map +1 -0
- package/lib/hooks/useSPFxUserPhoto.js +346 -0
- package/lib/hooks/useSPFxUserPhoto.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +3 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/resize-observer.internal.d.ts +10 -0
- package/lib/utils/resize-observer.internal.d.ts.map +1 -0
- package/lib/utils/resize-observer.internal.js +34 -0
- package/lib/utils/resize-observer.internal.js.map +1 -0
- package/lib/utils/theme-subscription.internal.d.ts +11 -0
- package/lib/utils/theme-subscription.internal.d.ts.map +1 -0
- package/lib/utils/theme-subscription.internal.js +58 -0
- package/lib/utils/theme-subscription.internal.js.map +1 -0
- package/lib/utils/type-guards.internal.d.ts +35 -0
- package/lib/utils/type-guards.internal.d.ts.map +1 -0
- package/lib/utils/type-guards.internal.js +88 -0
- package/lib/utils/type-guards.internal.js.map +1 -0
- package/lib/webparts/spFxReactToolkitTest/SpFxReactToolkitTestWebPart.d.ts +13 -0
- package/lib/webparts/spFxReactToolkitTest/SpFxReactToolkitTestWebPart.d.ts.map +1 -0
- package/lib/webparts/spFxReactToolkitTest/SpFxReactToolkitTestWebPart.js +67 -0
- package/lib/webparts/spFxReactToolkitTest/SpFxReactToolkitTestWebPart.js.map +1 -0
- package/lib/webparts/spFxReactToolkitTest/SpFxReactToolkitTestWebPart.manifest.json +21 -0
- package/lib/webparts/spFxReactToolkitTest/assets/welcome-dark.png +0 -0
- package/lib/webparts/spFxReactToolkitTest/assets/welcome-light.png +0 -0
- package/lib/webparts/spFxReactToolkitTest/components/ISpFxReactToolkitTestProps.d.ts +8 -0
- package/lib/webparts/spFxReactToolkitTest/components/ISpFxReactToolkitTestProps.d.ts.map +1 -0
- package/lib/webparts/spFxReactToolkitTest/components/ISpFxReactToolkitTestProps.js +2 -0
- package/lib/webparts/spFxReactToolkitTest/components/ISpFxReactToolkitTestProps.js.map +1 -0
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.d.ts +8 -0
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.d.ts.map +1 -0
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.js +1351 -0
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.js.map +1 -0
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.module.css +2 -0
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.module.scss.d.ts +18 -0
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.module.scss.d.ts.map +1 -0
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.module.scss.js +19 -0
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.module.scss.js.map +1 -0
- package/lib/webparts/spFxReactToolkitTest/loc/en-us.js +16 -0
- package/package.json +95 -0
package/README.md
ADDED
|
@@ -0,0 +1,2012 @@
|
|
|
1
|
+
# SPFx React Toolkit
|
|
2
|
+
|
|
3
|
+
> A comprehensive React runtime and hooks library for SharePoint Framework (SPFx) with 33+ type-safe hooks. Simplifies SPFx development with instance-scoped state isolation and ergonomic hooks API across WebParts, Extensions, and Command Sets.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [Features](#features)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [WebPart Setup](#webpart-setup)
|
|
16
|
+
- [ApplicationCustomizer Setup](#applicationcustomizer-setup)
|
|
17
|
+
- [FieldCustomizer Setup](#fieldcustomizer-setup)
|
|
18
|
+
- [ListView CommandSet Setup](#listview-commandset-setup)
|
|
19
|
+
- [Using Hooks in Components](#using-hooks-in-components)
|
|
20
|
+
- [Core API](#core-api)
|
|
21
|
+
- [Provider Components](#provider-components)
|
|
22
|
+
- [TypeScript Types](#typescript-types)
|
|
23
|
+
- [Hooks API](#hooks-api)
|
|
24
|
+
- [Context & Configuration](#context--configuration)
|
|
25
|
+
- [User & Site Information](#user--site-information)
|
|
26
|
+
- [UI & Layout](#ui--layout)
|
|
27
|
+
- [Storage](#storage)
|
|
28
|
+
- [HTTP Clients](#http-clients)
|
|
29
|
+
- [Performance & Diagnostics](#performance--diagnostics)
|
|
30
|
+
- [Permissions & Security](#permissions--security)
|
|
31
|
+
- [Environment & Platform](#environment--platform)
|
|
32
|
+
- [PnPjs Integration (Optional)](#pnpjs-integration-optional)
|
|
33
|
+
- [Installation](#pnpjs-installation)
|
|
34
|
+
- [Available PnP Hooks](#available-pnp-hooks)
|
|
35
|
+
- [TypeScript Support](#typescript-support)
|
|
36
|
+
- [Architecture](#architecture)
|
|
37
|
+
- [Best Practices](#best-practices)
|
|
38
|
+
- [Troubleshooting](#troubleshooting)
|
|
39
|
+
- [Compatibility](#compatibility)
|
|
40
|
+
- [License](#license)
|
|
41
|
+
- [Contributing](#contributing)
|
|
42
|
+
- [Support](#support)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Overview
|
|
47
|
+
|
|
48
|
+
**SPFx React Toolkit** is a comprehensive React runtime and hooks library for SharePoint Framework (SPFx) development. It provides a single `SPFxProvider` component that wraps your application and enables access to 33+ strongly-typed, production-ready hooks for seamless integration with SPFx context, properties, HTTP clients, permissions, storage, performance tracking, and more.
|
|
49
|
+
|
|
50
|
+
Built on [Jotai](https://jotai.org/) atomic state management, this toolkit delivers per-instance state isolation, automatic synchronization, and an ergonomic React Hooks API that works across all SPFx component types: **WebParts**, **Application Customizers**, **Field Customizers**, and **ListView Command Sets**.
|
|
51
|
+
|
|
52
|
+
### Why SPFx React Toolkit?
|
|
53
|
+
|
|
54
|
+
- **💪 Type-Safe** - Full TypeScript support with zero `any` usage
|
|
55
|
+
- **⚡ Optimized** - Jotai atomic state with per-instance scoping
|
|
56
|
+
- **🔄 Auto-Sync** - Bidirectional synchronization
|
|
57
|
+
- **🎨 Universal** - Works with all SPFx component types
|
|
58
|
+
- **📦 Modular** - Tree-shakeable, minimal bundle impact
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Features
|
|
63
|
+
|
|
64
|
+
- ✅ **Automatic Context Detection** - Detects WebPart, ApplicationCustomizer, CommandSet, or FieldCustomizer
|
|
65
|
+
- ✅ **33+ React Hooks** - Comprehensive API surface for all SPFx capabilities
|
|
66
|
+
- ✅ **Type-Safe** - Full TypeScript inference with strict typing
|
|
67
|
+
- ✅ **Instance Isolation** - State scoped per SPFx instance (multi-instance support)
|
|
68
|
+
- ✅ **Bidirectional Sync** - Properties automatically sync between UI and SPFx
|
|
69
|
+
- ✅ **PnPjs Integration** - Optional hooks for PnPjs v4 with type-safe filters
|
|
70
|
+
- ✅ **Performance Tracking** - Built-in hooks for performance measurement and logging
|
|
71
|
+
- ✅ **Cross-Platform** - Teams, SharePoint, and Local Workbench support
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install @apvee/spfx-react-toolkit
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Auto-Install (npm 7+):**
|
|
82
|
+
The following peer dependencies are automatically installed:
|
|
83
|
+
- **Jotai** v2+ - Atomic state management (lightweight ~3KB)
|
|
84
|
+
- **PnPjs** v4 - SharePoint API operations
|
|
85
|
+
|
|
86
|
+
### Peer Dependencies
|
|
87
|
+
|
|
88
|
+
All peer dependencies (`jotai`, `@pnp/sp`, `@pnp/core`, `@pnp/queryable`) are installed automatically with npm 7+. However:
|
|
89
|
+
|
|
90
|
+
- ✅ **Jotai** (~3KB) - Always included, core dependency for state management
|
|
91
|
+
- ✅ **PnP hooks not used?** - Tree-shaking removes unused PnP code (0 KB overhead)
|
|
92
|
+
- ✅ **PnP hooks used?** - Only imported parts included (~30-50 KB compressed)
|
|
93
|
+
- ✅ **No webpack errors** - All dependencies resolved
|
|
94
|
+
- ✅ **No duplicate installations** - npm reuses existing compatible versions
|
|
95
|
+
|
|
96
|
+
**All hooks available from single import:**
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import {
|
|
100
|
+
useSPFxProperties, // Core hooks
|
|
101
|
+
useSPFxContext,
|
|
102
|
+
useSPFxPnP, // PnP hooks
|
|
103
|
+
useSPFxPnPList
|
|
104
|
+
} from '@apvee/spfx-react-toolkit';
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Quick Start
|
|
110
|
+
|
|
111
|
+
### WebPart Setup
|
|
112
|
+
|
|
113
|
+
In your WebPart's `render()` method, wrap your component with `SPFxWebPartProvider`:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import * as React from 'react';
|
|
117
|
+
import * as ReactDom from 'react-dom';
|
|
118
|
+
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
|
119
|
+
import { SPFxWebPartProvider } from 'spfx-react-toolkit';
|
|
120
|
+
import MyComponent from './components/MyComponent';
|
|
121
|
+
|
|
122
|
+
export interface IMyWebPartProps {
|
|
123
|
+
title: string;
|
|
124
|
+
description: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default class MyWebPart extends BaseClientSideWebPart<IMyWebPartProps> {
|
|
128
|
+
public render(): void {
|
|
129
|
+
const element = React.createElement(
|
|
130
|
+
SPFxWebPartProvider,
|
|
131
|
+
{ instance: this }, // Type-safe, no casting needed
|
|
132
|
+
React.createElement(MyComponent)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
ReactDom.render(element, this.domElement);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected onDispose(): void {
|
|
139
|
+
ReactDom.unmountComponentAtNode(this.domElement);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### ApplicationCustomizer Setup
|
|
145
|
+
|
|
146
|
+
For Application Customizers, use `SPFxApplicationCustomizerProvider`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import * as React from 'react';
|
|
150
|
+
import * as ReactDom from 'react-dom';
|
|
151
|
+
import { BaseApplicationCustomizer, PlaceholderName } from '@microsoft/sp-application-base';
|
|
152
|
+
import { SPFxApplicationCustomizerProvider } from 'spfx-react-toolkit';
|
|
153
|
+
import MyHeaderComponent from './components/MyHeaderComponent';
|
|
154
|
+
|
|
155
|
+
export default class MyApplicationCustomizer extends BaseApplicationCustomizer<IMyProps> {
|
|
156
|
+
public onInit(): Promise<void> {
|
|
157
|
+
// Get placeholder
|
|
158
|
+
const placeholder = this.context.placeholderProvider.tryCreateContent(
|
|
159
|
+
PlaceholderName.Top
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (placeholder) {
|
|
163
|
+
const element = React.createElement(
|
|
164
|
+
SPFxApplicationCustomizerProvider,
|
|
165
|
+
{ instance: this },
|
|
166
|
+
React.createElement(MyHeaderComponent)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
ReactDom.render(element, placeholder.domElement);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return Promise.resolve();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### FieldCustomizer Setup
|
|
178
|
+
|
|
179
|
+
For Field Customizers, use `SPFxFieldCustomizerProvider`:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import * as React from 'react';
|
|
183
|
+
import * as ReactDom from 'react-dom';
|
|
184
|
+
import { BaseFieldCustomizer, IFieldCustomizerCellEventParameters } from '@microsoft/sp-listview-extensibility';
|
|
185
|
+
import { SPFxFieldCustomizerProvider } from 'spfx-react-toolkit';
|
|
186
|
+
import MyFieldRenderer from './components/MyFieldRenderer';
|
|
187
|
+
|
|
188
|
+
export default class MyFieldCustomizer extends BaseFieldCustomizer<IMyProps> {
|
|
189
|
+
public onRenderCell(event: IFieldCustomizerCellEventParameters): void {
|
|
190
|
+
const element = React.createElement(
|
|
191
|
+
SPFxFieldCustomizerProvider,
|
|
192
|
+
{ instance: this },
|
|
193
|
+
React.createElement(MyFieldRenderer, {
|
|
194
|
+
value: event.fieldValue,
|
|
195
|
+
listItem: event.listItem
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
ReactDom.render(element, event.domElement);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public onDisposeCell(event: IFieldCustomizerCellEventParameters): void {
|
|
203
|
+
ReactDom.unmountComponentAtNode(event.domElement);
|
|
204
|
+
super.onDisposeCell(event);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### ListView CommandSet Setup
|
|
210
|
+
|
|
211
|
+
For ListView Command Sets, use `SPFxListViewCommandSetProvider`:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import * as React from 'react';
|
|
215
|
+
import * as ReactDom from 'react-dom';
|
|
216
|
+
import { BaseListViewCommandSet, IListViewCommandSetExecuteEventParameters } from '@microsoft/sp-listview-extensibility';
|
|
217
|
+
import { Dialog } from '@microsoft/sp-dialog';
|
|
218
|
+
import { SPFxListViewCommandSetProvider } from 'spfx-react-toolkit';
|
|
219
|
+
import MyDialogComponent from './components/MyDialogComponent';
|
|
220
|
+
|
|
221
|
+
export default class MyCommandSet extends BaseListViewCommandSet<IMyProps> {
|
|
222
|
+
public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
|
|
223
|
+
switch (event.itemId) {
|
|
224
|
+
case 'COMMAND_1':
|
|
225
|
+
this._showDialog(event.selectedRows);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private _showDialog(selectedItems: any[]): void {
|
|
231
|
+
const dialogElement = document.createElement('div');
|
|
232
|
+
document.body.appendChild(dialogElement);
|
|
233
|
+
|
|
234
|
+
const element = React.createElement(
|
|
235
|
+
SPFxListViewCommandSetProvider,
|
|
236
|
+
{ instance: this },
|
|
237
|
+
React.createElement(MyDialogComponent, {
|
|
238
|
+
items: selectedItems,
|
|
239
|
+
onDismiss: () => {
|
|
240
|
+
ReactDom.unmountComponentAtNode(dialogElement);
|
|
241
|
+
document.body.removeChild(dialogElement);
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
ReactDom.render(element, dialogElement);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Using Hooks in Components
|
|
252
|
+
|
|
253
|
+
Once wrapped with a Provider, access SPFx capabilities via hooks:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import * as React from 'react';
|
|
257
|
+
import {
|
|
258
|
+
useSPFxProperties,
|
|
259
|
+
useSPFxDisplayMode,
|
|
260
|
+
useSPFxThemeInfo,
|
|
261
|
+
useSPFxUserInfo,
|
|
262
|
+
useSPFxSiteInfo,
|
|
263
|
+
} from 'spfx-react-toolkit';
|
|
264
|
+
|
|
265
|
+
interface IMyWebPartProps {
|
|
266
|
+
title: string;
|
|
267
|
+
description: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const MyComponent: React.FC = () => {
|
|
271
|
+
// Access and update properties
|
|
272
|
+
const { properties, setProperties } = useSPFxProperties<IMyWebPartProps>();
|
|
273
|
+
|
|
274
|
+
// Check display mode
|
|
275
|
+
const { isEdit } = useSPFxDisplayMode();
|
|
276
|
+
|
|
277
|
+
// Get theme colors
|
|
278
|
+
const theme = useSPFxThemeInfo();
|
|
279
|
+
|
|
280
|
+
// Get user information
|
|
281
|
+
const { displayName, email } = useSPFxUserInfo();
|
|
282
|
+
|
|
283
|
+
// Get site information
|
|
284
|
+
const { title: siteTitle, webUrl } = useSPFxSiteInfo();
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div style={{
|
|
288
|
+
backgroundColor: theme?.semanticColors?.bodyBackground,
|
|
289
|
+
color: theme?.semanticColors?.bodyText,
|
|
290
|
+
padding: '20px'
|
|
291
|
+
}}>
|
|
292
|
+
<h1>{properties?.title || 'Default Title'}</h1>
|
|
293
|
+
<p>{properties?.description}</p>
|
|
294
|
+
<p>Welcome, {displayName} ({email})</p>
|
|
295
|
+
<p>Site: {siteTitle} - {webUrl}</p>
|
|
296
|
+
|
|
297
|
+
{isEdit && (
|
|
298
|
+
<button onClick={() => setProperties({ title: 'Updated Title' })}>
|
|
299
|
+
Update Title
|
|
300
|
+
</button>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export default MyComponent;
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Core API
|
|
312
|
+
|
|
313
|
+
### Provider Components
|
|
314
|
+
|
|
315
|
+
The toolkit provides **4 type-safe Provider components**, one for each SPFx component type. Each Provider automatically detects the component kind, initializes instance-scoped state, and enables all hooks.
|
|
316
|
+
|
|
317
|
+
#### `SPFxWebPartProvider<TProps>`
|
|
318
|
+
|
|
319
|
+
Type-safe provider for **WebParts**.
|
|
320
|
+
|
|
321
|
+
**Props:**
|
|
322
|
+
- `instance: BaseClientSideWebPart<TProps>` - WebPart instance
|
|
323
|
+
- `children?: React.ReactNode` - Child components
|
|
324
|
+
|
|
325
|
+
**Example:**
|
|
326
|
+
```typescript
|
|
327
|
+
import { SPFxWebPartProvider } from 'spfx-react-toolkit';
|
|
328
|
+
|
|
329
|
+
export default class MyWebPart extends BaseClientSideWebPart<IMyWebPartProps> {
|
|
330
|
+
public render(): void {
|
|
331
|
+
const element = React.createElement(
|
|
332
|
+
SPFxWebPartProvider,
|
|
333
|
+
{ instance: this },
|
|
334
|
+
React.createElement(MyComponent)
|
|
335
|
+
);
|
|
336
|
+
ReactDom.render(element, this.domElement);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### `SPFxApplicationCustomizerProvider<TProps>`
|
|
342
|
+
|
|
343
|
+
Type-safe provider for **Application Customizers**.
|
|
344
|
+
|
|
345
|
+
**Props:**
|
|
346
|
+
- `instance: BaseApplicationCustomizer<TProps>` - Application Customizer instance
|
|
347
|
+
- `children?: React.ReactNode` - Child components
|
|
348
|
+
|
|
349
|
+
**Example:**
|
|
350
|
+
```typescript
|
|
351
|
+
import { SPFxApplicationCustomizerProvider } from 'spfx-react-toolkit';
|
|
352
|
+
|
|
353
|
+
export default class MyApplicationCustomizer extends BaseApplicationCustomizer<IMyProps> {
|
|
354
|
+
public onInit(): Promise<void> {
|
|
355
|
+
const placeholder = this.context.placeholderProvider.tryCreateContent(
|
|
356
|
+
PlaceholderName.Top
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
if (placeholder) {
|
|
360
|
+
const element = React.createElement(
|
|
361
|
+
SPFxApplicationCustomizerProvider,
|
|
362
|
+
{ instance: this },
|
|
363
|
+
React.createElement(MyHeaderComponent)
|
|
364
|
+
);
|
|
365
|
+
ReactDom.render(element, placeholder.domElement);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return Promise.resolve();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### `SPFxFieldCustomizerProvider<TProps>`
|
|
374
|
+
|
|
375
|
+
Type-safe provider for **Field Customizers**.
|
|
376
|
+
|
|
377
|
+
**Props:**
|
|
378
|
+
- `instance: BaseFieldCustomizer<TProps>` - Field Customizer instance
|
|
379
|
+
- `children?: React.ReactNode` - Child components
|
|
380
|
+
|
|
381
|
+
**Example:**
|
|
382
|
+
```typescript
|
|
383
|
+
import { SPFxFieldCustomizerProvider } from 'spfx-react-toolkit';
|
|
384
|
+
|
|
385
|
+
export default class MyFieldCustomizer extends BaseFieldCustomizer<IMyProps> {
|
|
386
|
+
public onRenderCell(event: IFieldCustomizerCellEventParameters): void {
|
|
387
|
+
const element = React.createElement(
|
|
388
|
+
SPFxFieldCustomizerProvider,
|
|
389
|
+
{ instance: this },
|
|
390
|
+
React.createElement(MyFieldRenderer, { value: event.fieldValue })
|
|
391
|
+
);
|
|
392
|
+
ReactDom.render(element, event.domElement);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
#### `SPFxListViewCommandSetProvider<TProps>`
|
|
398
|
+
|
|
399
|
+
Type-safe provider for **ListView Command Sets**.
|
|
400
|
+
|
|
401
|
+
**Props:**
|
|
402
|
+
- `instance: BaseListViewCommandSet<TProps>` - ListView Command Set instance
|
|
403
|
+
- `children?: React.ReactNode` - Child components
|
|
404
|
+
|
|
405
|
+
**Example:**
|
|
406
|
+
```typescript
|
|
407
|
+
import { SPFxListViewCommandSetProvider } from 'spfx-react-toolkit';
|
|
408
|
+
|
|
409
|
+
export default class MyCommandSet extends BaseListViewCommandSet<IMyProps> {
|
|
410
|
+
public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
|
|
411
|
+
const element = React.createElement(
|
|
412
|
+
SPFxListViewCommandSetProvider,
|
|
413
|
+
{ instance: this },
|
|
414
|
+
React.createElement(MyDialogComponent)
|
|
415
|
+
);
|
|
416
|
+
// Render to dialog container
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### TypeScript Types
|
|
422
|
+
|
|
423
|
+
All Provider props and hook return types are fully typed and exported:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
import type {
|
|
427
|
+
// Provider Props
|
|
428
|
+
SPFxWebPartProviderProps,
|
|
429
|
+
SPFxApplicationCustomizerProviderProps,
|
|
430
|
+
SPFxFieldCustomizerProviderProps,
|
|
431
|
+
SPFxListViewCommandSetProviderProps,
|
|
432
|
+
|
|
433
|
+
// Core Context Types
|
|
434
|
+
HostKind, // 'WebPart' | 'AppCustomizer' | 'FieldCustomizer' | 'CommandSet' | 'ACE'
|
|
435
|
+
SPFxComponent, // Union of all SPFx component types
|
|
436
|
+
SPFxContextType, // Union of all SPFx context types
|
|
437
|
+
SPFxContextValue, // Context value: { instanceId, spfxContext, kind }
|
|
438
|
+
ContainerSize, // { width: number, height: number }
|
|
439
|
+
|
|
440
|
+
// Hook Return Types
|
|
441
|
+
SPFxPropertiesInfo, // useSPFxProperties
|
|
442
|
+
SPFxDisplayModeInfo, // useSPFxDisplayMode
|
|
443
|
+
SPFxInstanceInfo, // useSPFxInstanceInfo
|
|
444
|
+
SPFxEnvironmentInfo, // useSPFxEnvironmentInfo
|
|
445
|
+
SPFxPageTypeInfo, // useSPFxPageType
|
|
446
|
+
SPFxUserInfo, // useSPFxUserInfo
|
|
447
|
+
SPFxSiteInfo, // useSPFxSiteInfo
|
|
448
|
+
SPFxLocaleInfo, // useSPFxLocaleInfo
|
|
449
|
+
SPFxListInfo, // useSPFxListInfo
|
|
450
|
+
SPFxHubSiteInfo, // useSPFxHubSiteInfo
|
|
451
|
+
SPFxThemeInfo, // useSPFxThemeInfo
|
|
452
|
+
SPFxFluent9ThemeInfo, // useSPFxFluent9ThemeInfo
|
|
453
|
+
SPFxContainerInfo, // useSPFxContainerInfo
|
|
454
|
+
SPFxStorageHook, // useSPFxLocalStorage / useSPFxSessionStorage
|
|
455
|
+
SPFxPerformanceInfo, // useSPFxPerformance
|
|
456
|
+
SPFxPerfResult, // Performance measurement result
|
|
457
|
+
SPFxLoggerInfo, // useSPFxLogger
|
|
458
|
+
LogEntry, // Log entry structure
|
|
459
|
+
LogLevel, // Log levels
|
|
460
|
+
SPFxCorrelationInfo, // useSPFxCorrelationInfo
|
|
461
|
+
SPFxPermissionsInfo, // useSPFxPermissions
|
|
462
|
+
SPFxTeamsInfo, // useSPFxTeams
|
|
463
|
+
TeamsTheme, // Teams theme type
|
|
464
|
+
SPFxOneDriveAppDataResult, // useSPFxOneDriveAppData
|
|
465
|
+
|
|
466
|
+
// PnP Types (if using PnPjs integration)
|
|
467
|
+
PnPContextInfo, // useSPFxPnPContext
|
|
468
|
+
SPFxPnPInfo, // useSPFxPnP
|
|
469
|
+
SPFxPnPListInfo, // useSPFxPnPList
|
|
470
|
+
SPFxPnPSearchInfo, // useSPFxPnPSearch
|
|
471
|
+
} from 'spfx-react-toolkit';
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Hooks API
|
|
477
|
+
|
|
478
|
+
The toolkit provides **33 specialized hooks** organized by functionality. All hooks are type-safe, memoized, and automatically access the instance-scoped state.
|
|
479
|
+
|
|
480
|
+
### Context & Configuration
|
|
481
|
+
|
|
482
|
+
#### `useSPFxPageContext()`
|
|
483
|
+
|
|
484
|
+
Access the full SharePoint PageContext object containing site, web, user, list, and Teams information.
|
|
485
|
+
|
|
486
|
+
**Returns:** `PageContext`
|
|
487
|
+
|
|
488
|
+
**Example:**
|
|
489
|
+
```typescript
|
|
490
|
+
const pageContext = useSPFxPageContext();
|
|
491
|
+
|
|
492
|
+
console.log(pageContext.web.title);
|
|
493
|
+
console.log(pageContext.web.absoluteUrl);
|
|
494
|
+
console.log(pageContext.user.displayName);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
#### `useSPFxProperties<T>()`
|
|
500
|
+
|
|
501
|
+
Access and manage SPFx properties with type-safe partial updates and automatic bidirectional synchronization with the Property Pane.
|
|
502
|
+
|
|
503
|
+
**Returns:** `SPFxPropertiesInfo<T>`
|
|
504
|
+
- `properties: T | undefined` - Current properties object
|
|
505
|
+
- `setProperties: (updates: Partial<T>) => void` - Partial merge update
|
|
506
|
+
- `updateProperties: (updater: (current: T | undefined) => T) => void` - Updater function pattern
|
|
507
|
+
|
|
508
|
+
**Features:**
|
|
509
|
+
- ✅ Type-safe with generics
|
|
510
|
+
- ✅ Partial updates (shallow merge)
|
|
511
|
+
- ✅ Updater function pattern (like React setState)
|
|
512
|
+
- ✅ Automatic bidirectional sync with SPFx
|
|
513
|
+
- ✅ Property Pane refresh for WebParts
|
|
514
|
+
|
|
515
|
+
**Example:**
|
|
516
|
+
```typescript
|
|
517
|
+
interface IMyWebPartProps {
|
|
518
|
+
title: string;
|
|
519
|
+
description: string;
|
|
520
|
+
listId?: string;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function MyComponent() {
|
|
524
|
+
const { properties, setProperties, updateProperties } =
|
|
525
|
+
useSPFxProperties<IMyWebPartProps>();
|
|
526
|
+
|
|
527
|
+
return (
|
|
528
|
+
<div>
|
|
529
|
+
<h1>{properties?.title ?? 'Default Title'}</h1>
|
|
530
|
+
<p>{properties?.description}</p>
|
|
531
|
+
|
|
532
|
+
{/* Partial update */}
|
|
533
|
+
<button onClick={() => setProperties({ title: 'New Title' })}>
|
|
534
|
+
Update Title
|
|
535
|
+
</button>
|
|
536
|
+
|
|
537
|
+
{/* Updater function */}
|
|
538
|
+
<button onClick={() => updateProperties(prev => ({
|
|
539
|
+
...prev,
|
|
540
|
+
title: (prev?.title ?? '') + ' Updated'
|
|
541
|
+
}))}>
|
|
542
|
+
Append to Title
|
|
543
|
+
</button>
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
#### `useSPFxDisplayMode()`
|
|
552
|
+
|
|
553
|
+
Access display mode (Read/Edit) for conditional rendering. Display mode is readonly and controlled by SharePoint.
|
|
554
|
+
|
|
555
|
+
**Returns:** `SPFxDisplayModeInfo`
|
|
556
|
+
- `mode: DisplayMode` - Current display mode (Read/Edit)
|
|
557
|
+
- `isEdit: boolean` - True if in Edit mode
|
|
558
|
+
- `isRead: boolean` - True if in Read mode
|
|
559
|
+
|
|
560
|
+
**Example:**
|
|
561
|
+
```typescript
|
|
562
|
+
function MyComponent() {
|
|
563
|
+
const { isEdit, isRead } = useSPFxDisplayMode();
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
<div>
|
|
567
|
+
<p>Mode: {isEdit ? 'Editing' : 'Reading'}</p>
|
|
568
|
+
{isEdit && <EditControls />}
|
|
569
|
+
{isRead && <ReadOnlyView />}
|
|
570
|
+
</div>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
#### `useSPFxInstanceInfo()`
|
|
578
|
+
|
|
579
|
+
Get unique instance ID and component kind for debugging, logging, and conditional logic.
|
|
580
|
+
|
|
581
|
+
**Returns:** `SPFxInstanceInfo`
|
|
582
|
+
- `id: string` - Unique identifier for this SPFx instance
|
|
583
|
+
- `kind: HostKind` - Component type: `'WebPart' | 'AppCustomizer' | 'FieldCustomizer' | 'CommandSet' | 'ACE'`
|
|
584
|
+
|
|
585
|
+
**Example:**
|
|
586
|
+
```typescript
|
|
587
|
+
function MyComponent() {
|
|
588
|
+
const { id, kind } = useSPFxInstanceInfo();
|
|
589
|
+
|
|
590
|
+
console.log(`Instance ID: ${id}`);
|
|
591
|
+
console.log(`Component Type: ${kind}`);
|
|
592
|
+
|
|
593
|
+
if (kind === 'WebPart') {
|
|
594
|
+
return <WebPartView />;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return <ExtensionView />;
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
#### `useSPFxServiceScope()`
|
|
604
|
+
|
|
605
|
+
Access SPFx ServiceScope for advanced service consumption and dependency injection.
|
|
606
|
+
|
|
607
|
+
**Returns:** `ServiceScope | undefined`
|
|
608
|
+
|
|
609
|
+
**Example:**
|
|
610
|
+
```typescript
|
|
611
|
+
import { ServiceKey, ServiceScope } from '@microsoft/sp-core-library';
|
|
612
|
+
|
|
613
|
+
const MyServiceKey = ServiceKey.create<IMyService>('my-service', MyService);
|
|
614
|
+
|
|
615
|
+
function MyComponent() {
|
|
616
|
+
const serviceScope = useSPFxServiceScope();
|
|
617
|
+
|
|
618
|
+
const myService = serviceScope?.consume(MyServiceKey);
|
|
619
|
+
|
|
620
|
+
// Use service...
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
### User & Site Information
|
|
627
|
+
|
|
628
|
+
#### `useSPFxUserInfo()`
|
|
629
|
+
|
|
630
|
+
Access current user information including display name, email, login name, and guest status.
|
|
631
|
+
|
|
632
|
+
**Returns:** `SPFxUserInfo`
|
|
633
|
+
- `displayName: string` - User display name
|
|
634
|
+
- `email: string | undefined` - User email address
|
|
635
|
+
- `loginName: string` - User login name (e.g., "domain\\user" or email)
|
|
636
|
+
- `isExternal: boolean` - Whether user is an external guest
|
|
637
|
+
|
|
638
|
+
**Example:**
|
|
639
|
+
```typescript
|
|
640
|
+
function MyComponent() {
|
|
641
|
+
const { displayName, email, isExternal } = useSPFxUserInfo();
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<div>
|
|
645
|
+
<h2>Welcome, {displayName}!</h2>
|
|
646
|
+
{email && <p>Email: {email}</p>}
|
|
647
|
+
{isExternal && <Badge>Guest User</Badge>}
|
|
648
|
+
</div>
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
#### `useSPFxSiteInfo()`
|
|
656
|
+
|
|
657
|
+
Access comprehensive site collection and web information with flat, predictable property naming.
|
|
658
|
+
|
|
659
|
+
**Returns:** `SPFxSiteInfo`
|
|
660
|
+
|
|
661
|
+
**Web Properties (primary context - 90% use case):**
|
|
662
|
+
- `webId: string` - Web ID (GUID)
|
|
663
|
+
- `webUrl: string` - Web absolute URL
|
|
664
|
+
- `webServerRelativeUrl: string` - Web server-relative URL
|
|
665
|
+
- `title: string` - Web display name (most commonly used)
|
|
666
|
+
- `languageId: number` - Web language (LCID)
|
|
667
|
+
- `logoUrl?: string` - Site logo URL (for branding)
|
|
668
|
+
|
|
669
|
+
**Site Collection Properties (parent context - specialized):**
|
|
670
|
+
- `siteId: string` - Site collection ID (GUID)
|
|
671
|
+
- `siteUrl: string` - Site collection absolute URL
|
|
672
|
+
- `siteServerRelativeUrl: string` - Site collection server-relative URL
|
|
673
|
+
- `siteClassification?: string` - Enterprise classification (e.g., "Confidential", "Public")
|
|
674
|
+
- `siteGroup?: SPFxGroupInfo` - Microsoft 365 Group info (if group-connected)
|
|
675
|
+
|
|
676
|
+
**Example:**
|
|
677
|
+
```typescript
|
|
678
|
+
function SiteHeader() {
|
|
679
|
+
const {
|
|
680
|
+
title, // Web title (most common)
|
|
681
|
+
webUrl, // Web URL
|
|
682
|
+
logoUrl, // Site logo
|
|
683
|
+
siteClassification, // Enterprise classification
|
|
684
|
+
siteGroup // M365 Group info
|
|
685
|
+
} = useSPFxSiteInfo();
|
|
686
|
+
|
|
687
|
+
return (
|
|
688
|
+
<header>
|
|
689
|
+
{logoUrl && <img src={logoUrl} alt="Site logo" />}
|
|
690
|
+
<h1>{title}</h1>
|
|
691
|
+
<a href={webUrl}>Visit Site</a>
|
|
692
|
+
|
|
693
|
+
{siteClassification && (
|
|
694
|
+
<Label>Classification: {siteClassification}</Label>
|
|
695
|
+
)}
|
|
696
|
+
|
|
697
|
+
{siteGroup && (
|
|
698
|
+
<Badge>
|
|
699
|
+
{siteGroup.isPublic ? 'Public Team' : 'Private Team'}
|
|
700
|
+
</Badge>
|
|
701
|
+
)}
|
|
702
|
+
</header>
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
#### `useSPFxLocaleInfo()`
|
|
710
|
+
|
|
711
|
+
Access locale and regional settings for internationalization (i18n) with direct Intl API compatibility.
|
|
712
|
+
|
|
713
|
+
**Returns:** `SPFxLocaleInfo`
|
|
714
|
+
- `locale: string` - Current content locale (e.g., "en-US", "it-IT")
|
|
715
|
+
- `uiLocale: string` - Current UI language locale
|
|
716
|
+
- `timeZone?: SPFxTimeZone` - Time zone information (preview API)
|
|
717
|
+
- `isRtl: boolean` - Whether language is right-to-left
|
|
718
|
+
|
|
719
|
+
**Example:**
|
|
720
|
+
```typescript
|
|
721
|
+
function DateDisplay() {
|
|
722
|
+
const { locale, isRtl, timeZone } = useSPFxLocaleInfo();
|
|
723
|
+
|
|
724
|
+
const formatDate = (date: Date) => {
|
|
725
|
+
return new Intl.DateTimeFormat(locale, {
|
|
726
|
+
dateStyle: 'full',
|
|
727
|
+
timeStyle: 'long'
|
|
728
|
+
}).format(date);
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
<div dir={isRtl ? 'rtl' : 'ltr'}>
|
|
733
|
+
<p>{formatDate(new Date())}</p>
|
|
734
|
+
{timeZone && (
|
|
735
|
+
<p>Time Zone: {timeZone.description} (UTC {timeZone.offset/60})</p>
|
|
736
|
+
)}
|
|
737
|
+
</div>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
#### `useSPFxListInfo()`
|
|
745
|
+
|
|
746
|
+
Access list information when component is rendered in a list context (Field Customizers, list-scoped components).
|
|
747
|
+
|
|
748
|
+
**Returns:** `SPFxListInfo | undefined`
|
|
749
|
+
- `id: string` - List ID (GUID)
|
|
750
|
+
- `title: string` - List title
|
|
751
|
+
- `serverRelativeUrl: string` - List server-relative URL
|
|
752
|
+
- `baseTemplate?: number` - List template type (e.g., 100 for Generic List, 101 for Document Library)
|
|
753
|
+
- `isDocumentLibrary?: boolean` - Whether list is a document library
|
|
754
|
+
|
|
755
|
+
**Example:**
|
|
756
|
+
```typescript
|
|
757
|
+
function FieldRenderer() {
|
|
758
|
+
const list = useSPFxListInfo();
|
|
759
|
+
|
|
760
|
+
if (!list) {
|
|
761
|
+
return <div>Not in list context</div>;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return (
|
|
765
|
+
<div>
|
|
766
|
+
<h3>{list.title}</h3>
|
|
767
|
+
<p>List ID: {list.id}</p>
|
|
768
|
+
{list.isDocumentLibrary && <Icon iconName="DocumentLibrary" />}
|
|
769
|
+
</div>
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
#### `useSPFxHubSiteInfo()`
|
|
777
|
+
|
|
778
|
+
Access Hub Site association information with automatic hub URL fetching via REST API.
|
|
779
|
+
|
|
780
|
+
**Returns:** `SPFxHubSiteInfo`
|
|
781
|
+
- `isHubSite: boolean` - Whether site is associated with a hub
|
|
782
|
+
- `hubSiteId?: string` - Hub site ID (GUID)
|
|
783
|
+
- `hubSiteUrl?: string` - Hub site URL (fetched asynchronously)
|
|
784
|
+
- `isLoading: boolean` - Loading state for hub URL fetch
|
|
785
|
+
- `error?: Error` - Error during hub URL fetch
|
|
786
|
+
|
|
787
|
+
**Example:**
|
|
788
|
+
```typescript
|
|
789
|
+
function HubNavigation() {
|
|
790
|
+
const { isHubSite, hubSiteUrl, isLoading } = useSPFxHubSiteInfo();
|
|
791
|
+
|
|
792
|
+
if (!isHubSite) return null;
|
|
793
|
+
|
|
794
|
+
if (isLoading) {
|
|
795
|
+
return <Spinner label="Loading hub info..." />;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return (
|
|
799
|
+
<nav>
|
|
800
|
+
<a href={hubSiteUrl}>← Back to Hub</a>
|
|
801
|
+
</nav>
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
### UI & Layout
|
|
809
|
+
|
|
810
|
+
#### `useSPFxThemeInfo()`
|
|
811
|
+
|
|
812
|
+
Access current SPFx theme (Fluent UI 8) with automatic updates when user switches themes.
|
|
813
|
+
|
|
814
|
+
**Returns:** `IReadonlyTheme | undefined`
|
|
815
|
+
|
|
816
|
+
**Example:**
|
|
817
|
+
```typescript
|
|
818
|
+
function MyComponent() {
|
|
819
|
+
const theme = useSPFxThemeInfo();
|
|
820
|
+
|
|
821
|
+
return (
|
|
822
|
+
<div style={{
|
|
823
|
+
backgroundColor: theme?.semanticColors?.bodyBackground,
|
|
824
|
+
color: theme?.semanticColors?.bodyText,
|
|
825
|
+
padding: '20px'
|
|
826
|
+
}}>
|
|
827
|
+
Themed Content
|
|
828
|
+
</div>
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
---
|
|
834
|
+
|
|
835
|
+
#### `useSPFxFluent9ThemeInfo()`
|
|
836
|
+
|
|
837
|
+
Access Fluent UI 9 theme with automatic Teams/SharePoint detection and theme conversion.
|
|
838
|
+
|
|
839
|
+
**Returns:** `SPFxFluent9ThemeInfo`
|
|
840
|
+
- `theme: Theme` - Fluent UI 9 theme object (ready for FluentProvider)
|
|
841
|
+
- `isTeams: boolean` - Whether running in Microsoft Teams
|
|
842
|
+
- `teamsTheme?: string` - Teams theme name ('default', 'dark', 'contrast')
|
|
843
|
+
|
|
844
|
+
**Priority order:**
|
|
845
|
+
1. Teams native themes (if in Teams)
|
|
846
|
+
2. SPFx theme converted to Fluent UI 9
|
|
847
|
+
3. Default webLightTheme
|
|
848
|
+
|
|
849
|
+
**Example:**
|
|
850
|
+
```typescript
|
|
851
|
+
import { FluentProvider } from '@fluentui/react-components';
|
|
852
|
+
|
|
853
|
+
function MyWebPart() {
|
|
854
|
+
const { theme, isTeams, teamsTheme } = useSPFxFluent9ThemeInfo();
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<FluentProvider theme={theme}>
|
|
858
|
+
<div>
|
|
859
|
+
<p>Running in: {isTeams ? 'Teams' : 'SharePoint'}</p>
|
|
860
|
+
{isTeams && <p>Teams theme: {teamsTheme}</p>}
|
|
861
|
+
<Button appearance="primary">Themed Button</Button>
|
|
862
|
+
</div>
|
|
863
|
+
</FluentProvider>
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
#### `useSPFxContainerSize()`
|
|
871
|
+
|
|
872
|
+
Get reactive container dimensions with Fluent UI 9 aligned breakpoints. Auto-updates on resize.
|
|
873
|
+
|
|
874
|
+
**Returns:** `SPFxContainerSizeInfo`
|
|
875
|
+
- `size: SPFxContainerSize` - Category: 'small' | 'medium' | 'large' | 'xLarge' | 'xxLarge' | 'xxxLarge'
|
|
876
|
+
- `isSmall: boolean` - 320-479px (mobile portrait)
|
|
877
|
+
- `isMedium: boolean` - 480-639px (mobile landscape)
|
|
878
|
+
- `isLarge: boolean` - 640-1023px (tablets)
|
|
879
|
+
- `isXLarge: boolean` - 1024-1365px (laptop, desktop)
|
|
880
|
+
- `isXXLarge: boolean` - 1366-1919px (wide desktop)
|
|
881
|
+
- `isXXXLarge: boolean` - ≥1920px (4K, ultra-wide)
|
|
882
|
+
- `width: number` - Actual width in pixels
|
|
883
|
+
- `height: number` - Actual height in pixels
|
|
884
|
+
|
|
885
|
+
**Example:**
|
|
886
|
+
```typescript
|
|
887
|
+
function ResponsiveWebPart() {
|
|
888
|
+
const { size, isSmall, isXXXLarge, width } = useSPFxContainerSize();
|
|
889
|
+
|
|
890
|
+
if (isSmall) {
|
|
891
|
+
return <CompactMobileView />;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (size === 'medium' || size === 'large') {
|
|
895
|
+
return <TabletView />;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (isXXXLarge) {
|
|
899
|
+
return <UltraWideView columns={6} />; // 4K/ultra-wide
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return <DesktopView columns={size === 'xxLarge' ? 4 : 3} />;
|
|
903
|
+
}
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
#### `useSPFxContainerInfo()`
|
|
909
|
+
|
|
910
|
+
Access container DOM element and size tracking.
|
|
911
|
+
|
|
912
|
+
**Returns:** `SPFxContainerInfo`
|
|
913
|
+
- `element: HTMLElement | undefined` - Container DOM element
|
|
914
|
+
- `size: ContainerSize | undefined` - `{ width: number, height: number }`
|
|
915
|
+
|
|
916
|
+
**Example:**
|
|
917
|
+
```typescript
|
|
918
|
+
function MyComponent() {
|
|
919
|
+
const { element, size } = useSPFxContainerInfo();
|
|
920
|
+
|
|
921
|
+
return (
|
|
922
|
+
<div>
|
|
923
|
+
{size && <p>Container: {size.width}px × {size.height}px</p>}
|
|
924
|
+
</div>
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
### Storage
|
|
932
|
+
|
|
933
|
+
#### `useSPFxLocalStorage<T>(key, defaultValue)`
|
|
934
|
+
|
|
935
|
+
Instance-scoped localStorage for persistent data across sessions. Automatically scoped per SPFx instance.
|
|
936
|
+
|
|
937
|
+
**Parameters:**
|
|
938
|
+
- `key: string` - Storage key (auto-prefixed with instance ID)
|
|
939
|
+
- `defaultValue: T` - Default value if not in storage
|
|
940
|
+
|
|
941
|
+
**Returns:** `SPFxStorageHook<T>`
|
|
942
|
+
- `value: T` - Current value
|
|
943
|
+
- `setValue: (value: T | ((prev: T) => T)) => void` - Set new value
|
|
944
|
+
- `remove: () => void` - Remove value (reset to default)
|
|
945
|
+
|
|
946
|
+
**Example:**
|
|
947
|
+
```typescript
|
|
948
|
+
function PreferencesPanel() {
|
|
949
|
+
const { value: viewMode, setValue: setViewMode } =
|
|
950
|
+
useSPFxLocalStorage('view-mode', 'grid');
|
|
951
|
+
|
|
952
|
+
return (
|
|
953
|
+
<div>
|
|
954
|
+
<p>View: {viewMode}</p>
|
|
955
|
+
<button onClick={() => setViewMode('list')}>List View</button>
|
|
956
|
+
<button onClick={() => setViewMode('grid')}>Grid View</button>
|
|
957
|
+
</div>
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
---
|
|
963
|
+
|
|
964
|
+
#### `useSPFxSessionStorage<T>(key, defaultValue)`
|
|
965
|
+
|
|
966
|
+
Instance-scoped sessionStorage for temporary data (current tab/session only). Automatically scoped per SPFx instance.
|
|
967
|
+
|
|
968
|
+
**Parameters:**
|
|
969
|
+
- `key: string` - Storage key (auto-prefixed with instance ID)
|
|
970
|
+
- `defaultValue: T` - Default value if not in storage
|
|
971
|
+
|
|
972
|
+
**Returns:** `SPFxStorageHook<T>`
|
|
973
|
+
- `value: T` - Current value
|
|
974
|
+
- `setValue: (value: T | ((prev: T) => T)) => void` - Set new value
|
|
975
|
+
- `remove: () => void` - Remove value (reset to default)
|
|
976
|
+
|
|
977
|
+
**Example:**
|
|
978
|
+
```typescript
|
|
979
|
+
function WizardComponent() {
|
|
980
|
+
const { value: step, setValue: setStep } =
|
|
981
|
+
useSPFxSessionStorage('wizard-step', 1);
|
|
982
|
+
|
|
983
|
+
return (
|
|
984
|
+
<div>
|
|
985
|
+
<p>Step: {step} of 5</p>
|
|
986
|
+
<button onClick={() => setStep(s => s + 1)}>Next</button>
|
|
987
|
+
<button onClick={() => setStep(s => s - 1)}>Back</button>
|
|
988
|
+
</div>
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
---
|
|
994
|
+
|
|
995
|
+
### HTTP Clients
|
|
996
|
+
|
|
997
|
+
#### `useSPFxSPHttpClient()`
|
|
998
|
+
|
|
999
|
+
Access SharePoint REST API client (SPHttpClient).
|
|
1000
|
+
|
|
1001
|
+
**Returns:** `SPFxSPHttpClientInfo`
|
|
1002
|
+
- `client: SPHttpClient | undefined` - SPHttpClient instance
|
|
1003
|
+
- `invoke: (fn) => Promise<T>` - Execute with error handling
|
|
1004
|
+
- `baseUrl: string` - Base URL for REST API calls
|
|
1005
|
+
|
|
1006
|
+
**Example:**
|
|
1007
|
+
```typescript
|
|
1008
|
+
function ListsViewer() {
|
|
1009
|
+
const { invoke, baseUrl } = useSPFxSPHttpClient();
|
|
1010
|
+
const [lists, setLists] = useState([]);
|
|
1011
|
+
|
|
1012
|
+
const fetchLists = async () => {
|
|
1013
|
+
const data = await invoke(async (client) => {
|
|
1014
|
+
const response = await client.get(
|
|
1015
|
+
`${baseUrl}/_api/web/lists`,
|
|
1016
|
+
SPHttpClient.configurations.v1
|
|
1017
|
+
);
|
|
1018
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1019
|
+
return (await response.json()).value;
|
|
1020
|
+
});
|
|
1021
|
+
setLists(data);
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
return (
|
|
1025
|
+
<div>
|
|
1026
|
+
<button onClick={fetchLists}>Load Lists</button>
|
|
1027
|
+
<ul>
|
|
1028
|
+
{lists.map(list => <li key={list.Id}>{list.Title}</li>)}
|
|
1029
|
+
</ul>
|
|
1030
|
+
</div>
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
#### `useSPFxMSGraphClient()`
|
|
1038
|
+
|
|
1039
|
+
Access Microsoft Graph API client.
|
|
1040
|
+
|
|
1041
|
+
**Returns:** `SPFxMSGraphClientInfo`
|
|
1042
|
+
- `client: MSGraphClientV3 | undefined` - MS Graph client instance
|
|
1043
|
+
- `invoke: (fn) => Promise<T>` - Execute with error handling
|
|
1044
|
+
|
|
1045
|
+
**Required API Permissions:** Configure in `package-solution.json`
|
|
1046
|
+
|
|
1047
|
+
**Example:**
|
|
1048
|
+
```typescript
|
|
1049
|
+
function UserProfile() {
|
|
1050
|
+
const { invoke } = useSPFxMSGraphClient();
|
|
1051
|
+
const [profile, setProfile] = useState(null);
|
|
1052
|
+
|
|
1053
|
+
const fetchProfile = async () => {
|
|
1054
|
+
const data = await invoke(async (client) => {
|
|
1055
|
+
return await client.api('/me').get();
|
|
1056
|
+
});
|
|
1057
|
+
setProfile(data);
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
useEffect(() => { fetchProfile(); }, []);
|
|
1061
|
+
|
|
1062
|
+
return profile && (
|
|
1063
|
+
<div>
|
|
1064
|
+
<h3>{profile.displayName}</h3>
|
|
1065
|
+
<p>{profile.mail}</p>
|
|
1066
|
+
</div>
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
---
|
|
1072
|
+
|
|
1073
|
+
#### `useSPFxAadHttpClient()`
|
|
1074
|
+
|
|
1075
|
+
Access Azure AD secured API client (AadHttpClient).
|
|
1076
|
+
|
|
1077
|
+
**Returns:** `SPFxAadHttpClientInfo`
|
|
1078
|
+
- `client: AadHttpClient | undefined` - AAD HTTP client instance
|
|
1079
|
+
- `invoke: (fn) => Promise<T>` - Execute with error handling
|
|
1080
|
+
|
|
1081
|
+
**Example:**
|
|
1082
|
+
```typescript
|
|
1083
|
+
function CustomApiCall() {
|
|
1084
|
+
const { invoke } = useSPFxAadHttpClient();
|
|
1085
|
+
const [data, setData] = useState(null);
|
|
1086
|
+
|
|
1087
|
+
const callApi = async () => {
|
|
1088
|
+
const result = await invoke(async (client) => {
|
|
1089
|
+
const response = await client.get(
|
|
1090
|
+
'https://api.contoso.com/data',
|
|
1091
|
+
AadHttpClient.configurations.v1
|
|
1092
|
+
);
|
|
1093
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1094
|
+
return await response.json();
|
|
1095
|
+
});
|
|
1096
|
+
setData(result);
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
return (
|
|
1100
|
+
<div>
|
|
1101
|
+
<button onClick={callApi}>Call API</button>
|
|
1102
|
+
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
|
1103
|
+
</div>
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
---
|
|
1109
|
+
|
|
1110
|
+
#### `useSPFxOneDriveAppData<T>(filename, folder?, autoFetch?)`
|
|
1111
|
+
|
|
1112
|
+
Manage JSON files in user's OneDrive appRoot folder with unified read/write operations.
|
|
1113
|
+
|
|
1114
|
+
**Parameters:**
|
|
1115
|
+
- `filename: string` - JSON filename
|
|
1116
|
+
- `folder?: string` - Folder namespace (optional, for isolation)
|
|
1117
|
+
- `autoFetch?: boolean` - Auto-fetch on mount (default: true)
|
|
1118
|
+
|
|
1119
|
+
**Returns:** `SPFxOneDriveAppDataResult<T>`
|
|
1120
|
+
- `data: T | undefined` - Current data
|
|
1121
|
+
- `isLoading: boolean` - Loading state
|
|
1122
|
+
- `isWriting: boolean` - Writing state
|
|
1123
|
+
- `error?: Error` - Error during operations
|
|
1124
|
+
- `write: (data: T) => Promise<void>` - Write data to file
|
|
1125
|
+
- `load: () => Promise<void>` - Manually load data
|
|
1126
|
+
- `isReady: boolean` - Client ready for operations
|
|
1127
|
+
|
|
1128
|
+
**Example:**
|
|
1129
|
+
```typescript
|
|
1130
|
+
interface UserSettings {
|
|
1131
|
+
theme: string;
|
|
1132
|
+
language: string;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function SettingsPanel() {
|
|
1136
|
+
const { data, write, isLoading, isWriting } =
|
|
1137
|
+
useSPFxOneDriveAppData<UserSettings>('settings.json');
|
|
1138
|
+
|
|
1139
|
+
if (isLoading) return <Spinner />;
|
|
1140
|
+
|
|
1141
|
+
const handleSave = async () => {
|
|
1142
|
+
await write({ theme: 'dark', language: 'en' });
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
return (
|
|
1146
|
+
<div>
|
|
1147
|
+
<p>Theme: {data?.theme}</p>
|
|
1148
|
+
<button onClick={handleSave} disabled={isWriting}>
|
|
1149
|
+
Save Settings
|
|
1150
|
+
</button>
|
|
1151
|
+
</div>
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
---
|
|
1157
|
+
|
|
1158
|
+
#### `useSPFxUserPhoto(options?)`
|
|
1159
|
+
|
|
1160
|
+
Load user profile photos from Microsoft Graph API. Supports current user or specific users by ID/email.
|
|
1161
|
+
|
|
1162
|
+
**Parameters:**
|
|
1163
|
+
- `options?: UserPhotoOptions` - Optional `{ userId?, email?, size?, autoFetch? }`
|
|
1164
|
+
|
|
1165
|
+
**Requires MS Graph Permissions:**
|
|
1166
|
+
- **User.Read**: For current user's photo
|
|
1167
|
+
- **User.ReadBasic.All**: For other users' photos
|
|
1168
|
+
|
|
1169
|
+
**Returns:** `SPFxUserPhotoInfo`
|
|
1170
|
+
- `photoUrl: string | undefined` - Photo URL or undefined
|
|
1171
|
+
- `isLoading: boolean` - Loading state
|
|
1172
|
+
- `error?: Error` - Error state
|
|
1173
|
+
- `reload: () => Promise<void>` - Manually reload photo
|
|
1174
|
+
|
|
1175
|
+
**Example:**
|
|
1176
|
+
```typescript
|
|
1177
|
+
// Current user
|
|
1178
|
+
const { photoUrl, isLoading } = useSPFxUserPhoto();
|
|
1179
|
+
|
|
1180
|
+
// Specific user by email
|
|
1181
|
+
const { photoUrl } = useSPFxUserPhoto({
|
|
1182
|
+
email: 'user@contoso.com',
|
|
1183
|
+
size: '96x96' // Options: 48x48, 64x64, 96x96, 120x120, 240x240, 360x360, 432x432, 504x504, 648x648
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
// Lazy loading
|
|
1187
|
+
const { photoUrl, reload, isLoading } = useSPFxUserPhoto({
|
|
1188
|
+
email: 'user@contoso.com',
|
|
1189
|
+
autoFetch: false
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
return (
|
|
1193
|
+
<div>
|
|
1194
|
+
{photoUrl ? (
|
|
1195
|
+
<img src={photoUrl} alt="Avatar" />
|
|
1196
|
+
) : (
|
|
1197
|
+
<button onClick={reload} disabled={isLoading}>
|
|
1198
|
+
{isLoading ? 'Loading...' : 'Load Photo'}
|
|
1199
|
+
</button>
|
|
1200
|
+
)}
|
|
1201
|
+
</div>
|
|
1202
|
+
);
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
#### `useSPFxTenantProperty<T>(key)`
|
|
1208
|
+
|
|
1209
|
+
Manage tenant-wide properties using SharePoint StorageEntity API with smart serialization.
|
|
1210
|
+
|
|
1211
|
+
**Parameters:**
|
|
1212
|
+
- `key: string` - Property key
|
|
1213
|
+
|
|
1214
|
+
**Returns:** `SPFxTenantPropertyInfo<T>`
|
|
1215
|
+
- `data: T | undefined` - Current property value
|
|
1216
|
+
- `description: string | undefined` - Property description
|
|
1217
|
+
- `isLoading: boolean` - Loading state
|
|
1218
|
+
- `error?: Error` - Error state
|
|
1219
|
+
- `write: (value: T, description?: string) => Promise<void>` - Write property
|
|
1220
|
+
- `remove: () => Promise<void>` - Remove property
|
|
1221
|
+
- `canWrite: boolean` - Whether user can write (requires Site Collection Admin on tenant app catalog)
|
|
1222
|
+
|
|
1223
|
+
**Requirements:**
|
|
1224
|
+
- Tenant app catalog must be provisioned
|
|
1225
|
+
- **Read**: Any authenticated user
|
|
1226
|
+
- **Write/Remove**: Must be Site Collection Administrator of tenant app catalog
|
|
1227
|
+
|
|
1228
|
+
**Smart Serialization:**
|
|
1229
|
+
- Primitives (string, number, boolean, null, bigint) → stored as string
|
|
1230
|
+
- Date objects → stored as ISO 8601 string
|
|
1231
|
+
- Objects/arrays → stored as JSON string
|
|
1232
|
+
|
|
1233
|
+
**Example:**
|
|
1234
|
+
```typescript
|
|
1235
|
+
// String property
|
|
1236
|
+
const { data, write, canWrite, isLoading } = useSPFxTenantProperty<string>('appVersion');
|
|
1237
|
+
|
|
1238
|
+
if (isLoading) return <Spinner />;
|
|
1239
|
+
|
|
1240
|
+
const handleUpdate = async () => {
|
|
1241
|
+
if (!canWrite) {
|
|
1242
|
+
alert('Insufficient permissions');
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
await write('2.0.1', 'Current application version');
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
return (
|
|
1249
|
+
<div>
|
|
1250
|
+
<p>Version: {data ?? 'Not Set'}</p>
|
|
1251
|
+
{canWrite && <button onClick={handleUpdate}>Update</button>}
|
|
1252
|
+
</div>
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
// Number property
|
|
1256
|
+
const { data: maxSize } = useSPFxTenantProperty<number>('maxUploadSize');
|
|
1257
|
+
await write(10485760, 'Max file size in bytes');
|
|
1258
|
+
|
|
1259
|
+
// Boolean property
|
|
1260
|
+
const { data: maintenance } = useSPFxTenantProperty<boolean>('maintenanceMode');
|
|
1261
|
+
if (maintenance) {
|
|
1262
|
+
return <MessageBar>System under maintenance</MessageBar>;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Complex object
|
|
1266
|
+
interface FeatureFlags {
|
|
1267
|
+
enableChat: boolean;
|
|
1268
|
+
enableAnalytics: boolean;
|
|
1269
|
+
maxUsers: number;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const { data, write } = useSPFxTenantProperty<FeatureFlags>('featureFlags');
|
|
1273
|
+
|
|
1274
|
+
await write({
|
|
1275
|
+
enableChat: true,
|
|
1276
|
+
enableAnalytics: false,
|
|
1277
|
+
maxUsers: 1000
|
|
1278
|
+
}, 'Global feature flags');
|
|
1279
|
+
|
|
1280
|
+
if (data?.enableChat) {
|
|
1281
|
+
return <ChatPanel />;
|
|
1282
|
+
}
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
---
|
|
1286
|
+
|
|
1287
|
+
### Performance & Diagnostics
|
|
1288
|
+
|
|
1289
|
+
#### `useSPFxPerformance()`
|
|
1290
|
+
|
|
1291
|
+
Performance measurement API with automatic SPFx context integration for monitoring and profiling.
|
|
1292
|
+
|
|
1293
|
+
**Returns:** `SPFxPerformanceInfo`
|
|
1294
|
+
- `mark: (name: string) => void` - Create performance mark
|
|
1295
|
+
- `measure: (name, startMark, endMark?) => SPFxPerfResult` - Measure duration between marks
|
|
1296
|
+
- `time: <T>(name, fn) => Promise<SPFxPerfResult<T>>` - Time async operations
|
|
1297
|
+
|
|
1298
|
+
**Example:**
|
|
1299
|
+
```typescript
|
|
1300
|
+
function DataLoader() {
|
|
1301
|
+
const { time } = useSPFxPerformance();
|
|
1302
|
+
const [data, setData] = useState(null);
|
|
1303
|
+
|
|
1304
|
+
const fetchData = async () => {
|
|
1305
|
+
const result = await time('fetch-data', async () => {
|
|
1306
|
+
const response = await fetch('/api/data');
|
|
1307
|
+
return response.json();
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
console.log(`Fetch took ${result.durationMs}ms`);
|
|
1311
|
+
setData(result.result);
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
return <button onClick={fetchData}>Load Data</button>;
|
|
1315
|
+
}
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
---
|
|
1319
|
+
|
|
1320
|
+
#### `useSPFxLogger(handler?)`
|
|
1321
|
+
|
|
1322
|
+
Structured logging with automatic SPFx context (instance ID, user, site, correlation ID).
|
|
1323
|
+
|
|
1324
|
+
**Parameters:**
|
|
1325
|
+
- `handler?: (entry: LogEntry) => void` - Optional custom log handler (e.g., Application Insights)
|
|
1326
|
+
|
|
1327
|
+
**Returns:** `SPFxLoggerInfo`
|
|
1328
|
+
- `debug: (message, extra?) => void` - Log debug message
|
|
1329
|
+
- `info: (message, extra?) => void` - Log info message
|
|
1330
|
+
- `warn: (message, extra?) => void` - Log warning message
|
|
1331
|
+
- `error: (message, extra?) => void` - Log error message
|
|
1332
|
+
|
|
1333
|
+
**Example:**
|
|
1334
|
+
```typescript
|
|
1335
|
+
function MyComponent() {
|
|
1336
|
+
const logger = useSPFxLogger();
|
|
1337
|
+
|
|
1338
|
+
const handleClick = () => {
|
|
1339
|
+
logger.info('Button clicked', { buttonId: 'save', timestamp: Date.now() });
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
const handleError = (error: Error) => {
|
|
1343
|
+
logger.error('Operation failed', {
|
|
1344
|
+
errorMessage: error.message,
|
|
1345
|
+
stack: error.stack
|
|
1346
|
+
});
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
return <button onClick={handleClick}>Save</button>;
|
|
1350
|
+
}
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
---
|
|
1354
|
+
|
|
1355
|
+
#### `useSPFxCorrelationInfo()`
|
|
1356
|
+
|
|
1357
|
+
Access correlation ID and tenant ID for distributed tracing and diagnostics.
|
|
1358
|
+
|
|
1359
|
+
**Returns:** `SPFxCorrelationInfo`
|
|
1360
|
+
- `correlationId?: string` - Correlation ID for tracking requests
|
|
1361
|
+
- `tenantId?: string` - Azure AD tenant ID
|
|
1362
|
+
|
|
1363
|
+
**Example:**
|
|
1364
|
+
```typescript
|
|
1365
|
+
function DiagnosticsPanel() {
|
|
1366
|
+
const { correlationId, tenantId } = useSPFxCorrelationInfo();
|
|
1367
|
+
|
|
1368
|
+
const logError = (error: Error) => {
|
|
1369
|
+
console.error('Error occurred', {
|
|
1370
|
+
message: error.message,
|
|
1371
|
+
correlationId,
|
|
1372
|
+
tenantId,
|
|
1373
|
+
timestamp: new Date().toISOString()
|
|
1374
|
+
});
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
return (
|
|
1378
|
+
<div>
|
|
1379
|
+
<p>Tenant: {tenantId}</p>
|
|
1380
|
+
<p>Correlation: {correlationId}</p>
|
|
1381
|
+
</div>
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
---
|
|
1387
|
+
|
|
1388
|
+
### Permissions & Security
|
|
1389
|
+
|
|
1390
|
+
#### `useSPFxPermissions()`
|
|
1391
|
+
|
|
1392
|
+
Check SharePoint permissions at site, web, and list levels with SPPermission enum helpers.
|
|
1393
|
+
|
|
1394
|
+
**Returns:** `SPFxPermissionsInfo`
|
|
1395
|
+
- `sitePermissions?: SPPermission` - Site collection permissions
|
|
1396
|
+
- `webPermissions?: SPPermission` - Web permissions
|
|
1397
|
+
- `listPermissions?: SPPermission` - List permissions (if in list context)
|
|
1398
|
+
- `hasWebPermission: (permission) => boolean` - Check web permission
|
|
1399
|
+
- `hasSitePermission: (permission) => boolean` - Check site permission
|
|
1400
|
+
- `hasListPermission: (permission) => boolean` - Check list permission
|
|
1401
|
+
|
|
1402
|
+
**Common Permissions:**
|
|
1403
|
+
- `SPPermission.manageWeb`
|
|
1404
|
+
- `SPPermission.addListItems`
|
|
1405
|
+
- `SPPermission.editListItems`
|
|
1406
|
+
- `SPPermission.deleteListItems`
|
|
1407
|
+
- `SPPermission.managePermissions`
|
|
1408
|
+
|
|
1409
|
+
**Example:**
|
|
1410
|
+
```typescript
|
|
1411
|
+
import { SPPermission } from '@microsoft/sp-page-context';
|
|
1412
|
+
|
|
1413
|
+
function AdminPanel() {
|
|
1414
|
+
const { hasWebPermission, hasListPermission } = useSPFxPermissions();
|
|
1415
|
+
|
|
1416
|
+
const canManage = hasWebPermission(SPPermission.manageWeb);
|
|
1417
|
+
const canAddItems = hasListPermission(SPPermission.addListItems);
|
|
1418
|
+
|
|
1419
|
+
return (
|
|
1420
|
+
<div>
|
|
1421
|
+
{canManage && <button>Manage Settings</button>}
|
|
1422
|
+
{canAddItems && <button>Add Item</button>}
|
|
1423
|
+
{!canManage && <p>Insufficient permissions</p>}
|
|
1424
|
+
</div>
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
#### `useSPFxCrossSitePermissions(siteUrl?, options?)`
|
|
1432
|
+
|
|
1433
|
+
Retrieve permissions for a different site/web/list (cross-site permission check).
|
|
1434
|
+
|
|
1435
|
+
**Parameters:**
|
|
1436
|
+
- `siteUrl?: string` - Target site URL (no fetch if undefined/empty - lazy loading)
|
|
1437
|
+
- `options?: SPFxCrossSitePermissionsOptions` - Optional `{ webUrl?, listId? }`
|
|
1438
|
+
|
|
1439
|
+
**Returns:** `SPFxCrossSitePermissionsInfo`
|
|
1440
|
+
- `sitePermissions?: SPPermission` - Site permissions
|
|
1441
|
+
- `webPermissions?: SPPermission` - Web permissions
|
|
1442
|
+
- `listPermissions?: SPPermission` - List permissions (if listId provided)
|
|
1443
|
+
- `hasWebPermission: (permission) => boolean` - Check web permission
|
|
1444
|
+
- `hasSitePermission: (permission) => boolean` - Check site permission
|
|
1445
|
+
- `hasListPermission: (permission) => boolean` - Check list permission
|
|
1446
|
+
- `isLoading: boolean` - Loading state
|
|
1447
|
+
- `error?: Error` - Error state
|
|
1448
|
+
|
|
1449
|
+
**Example:**
|
|
1450
|
+
```typescript
|
|
1451
|
+
import { SPPermission } from '@microsoft/sp-page-context';
|
|
1452
|
+
|
|
1453
|
+
function CrossSiteCheck() {
|
|
1454
|
+
const [targetUrl, setTargetUrl] = useState<string | undefined>();
|
|
1455
|
+
|
|
1456
|
+
const { hasWebPermission, isLoading, error } = useSPFxCrossSitePermissions(
|
|
1457
|
+
targetUrl,
|
|
1458
|
+
{ webUrl: 'https://contoso.sharepoint.com/sites/target/subweb' }
|
|
1459
|
+
);
|
|
1460
|
+
|
|
1461
|
+
// Trigger fetch
|
|
1462
|
+
const checkPermissions = () => {
|
|
1463
|
+
setTargetUrl('https://contoso.sharepoint.com/sites/target');
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
if (isLoading) return <Spinner />;
|
|
1467
|
+
if (error) return <MessageBar>{error.message}</MessageBar>;
|
|
1468
|
+
|
|
1469
|
+
const canAdd = hasWebPermission(SPPermission.addListItems);
|
|
1470
|
+
|
|
1471
|
+
return (
|
|
1472
|
+
<div>
|
|
1473
|
+
<button onClick={checkPermissions}>Check Permissions</button>
|
|
1474
|
+
{targetUrl && <p>Can add items: {canAdd ? 'Yes' : 'No'}</p>}
|
|
1475
|
+
</div>
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
---
|
|
1481
|
+
|
|
1482
|
+
### Environment & Platform
|
|
1483
|
+
|
|
1484
|
+
#### `useSPFxEnvironmentInfo()`
|
|
1485
|
+
|
|
1486
|
+
Detect execution environment (Local, SharePoint, Teams, Office, Outlook).
|
|
1487
|
+
|
|
1488
|
+
**Returns:** `SPFxEnvironmentInfo`
|
|
1489
|
+
- `type: SPFxEnvironmentType` - 'Local' | 'SharePoint' | 'SharePointOnPrem' | 'Teams' | 'Office' | 'Outlook'
|
|
1490
|
+
- `isLocal: boolean` - Running in local workbench
|
|
1491
|
+
- `isWorkbench: boolean` - Running in any workbench
|
|
1492
|
+
- `isSharePoint: boolean` - SharePoint Online
|
|
1493
|
+
- `isSharePointOnPrem: boolean` - SharePoint On-Premises
|
|
1494
|
+
- `isTeams: boolean` - Microsoft Teams
|
|
1495
|
+
- `isOffice: boolean` - Office application
|
|
1496
|
+
- `isOutlook: boolean` - Outlook
|
|
1497
|
+
|
|
1498
|
+
**Example:**
|
|
1499
|
+
```typescript
|
|
1500
|
+
function AdaptiveUI() {
|
|
1501
|
+
const { type, isTeams, isLocal } = useSPFxEnvironmentInfo();
|
|
1502
|
+
|
|
1503
|
+
if (isLocal) {
|
|
1504
|
+
return <DevModeBanner />;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (isTeams) {
|
|
1508
|
+
return <TeamsOptimizedUI />;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return <SharePointUI />;
|
|
1512
|
+
}
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
---
|
|
1516
|
+
|
|
1517
|
+
#### `useSPFxPageType()`
|
|
1518
|
+
|
|
1519
|
+
Detect SharePoint page type (modern site page, classic, list page, etc.).
|
|
1520
|
+
|
|
1521
|
+
**Returns:** `SPFxPageTypeInfo`
|
|
1522
|
+
- `pageType: SPFxPageType` - 'sitePage' | 'webPartPage' | 'listPage' | 'listFormPage' | 'profilePage' | 'searchPage' | 'unknown'
|
|
1523
|
+
- `isModernPage: boolean` - True for modern site pages
|
|
1524
|
+
- `isSitePage: boolean` - Site page (modern)
|
|
1525
|
+
- `isListPage: boolean` - List view page
|
|
1526
|
+
- `isListFormPage: boolean` - List form page
|
|
1527
|
+
- `isWebPartPage: boolean` - Classic web part page
|
|
1528
|
+
|
|
1529
|
+
**Example:**
|
|
1530
|
+
```typescript
|
|
1531
|
+
function FeatureGate() {
|
|
1532
|
+
const { isModernPage, isSitePage } = useSPFxPageType();
|
|
1533
|
+
|
|
1534
|
+
if (!isModernPage) {
|
|
1535
|
+
return <div>This feature requires a modern page</div>;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
return isSitePage ? <ModernFeature /> : <ClassicFallback />;
|
|
1539
|
+
}
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
---
|
|
1543
|
+
|
|
1544
|
+
#### `useSPFxTeams()`
|
|
1545
|
+
|
|
1546
|
+
Access Microsoft Teams context with automatic SDK initialization (v1 and v2 compatible).
|
|
1547
|
+
|
|
1548
|
+
**Returns:** `SPFxTeamsInfo`
|
|
1549
|
+
- `supported: boolean` - Whether Teams context is available
|
|
1550
|
+
- `context?: unknown` - Teams context object (team, channel, user info)
|
|
1551
|
+
- `theme?: TeamsTheme` - 'default' | 'dark' | 'highContrast'
|
|
1552
|
+
|
|
1553
|
+
**Example:**
|
|
1554
|
+
```typescript
|
|
1555
|
+
function TeamsIntegration() {
|
|
1556
|
+
const { supported, context, theme } = useSPFxTeams();
|
|
1557
|
+
|
|
1558
|
+
if (!supported) {
|
|
1559
|
+
return <div>Not running in Teams</div>;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const teamsContext = context as {
|
|
1563
|
+
team?: { displayName: string };
|
|
1564
|
+
channel?: { displayName: string };
|
|
1565
|
+
};
|
|
1566
|
+
|
|
1567
|
+
return (
|
|
1568
|
+
<div className={`teams-theme-${theme}`}>
|
|
1569
|
+
<h3>Team: {teamsContext.team?.displayName}</h3>
|
|
1570
|
+
<p>Channel: {teamsContext.channel?.displayName}</p>
|
|
1571
|
+
</div>
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
---
|
|
1577
|
+
|
|
1578
|
+
## PnPjs Integration (Optional)
|
|
1579
|
+
|
|
1580
|
+
This toolkit provides optional hooks for working with **PnPjs v4** for SharePoint REST API operations.
|
|
1581
|
+
|
|
1582
|
+
### <a id="pnpjs-installation"></a>Installation
|
|
1583
|
+
|
|
1584
|
+
All dependencies (Jotai + PnPjs) are **peer dependencies** and installed automatically:
|
|
1585
|
+
|
|
1586
|
+
```bash
|
|
1587
|
+
npm install @apvee/spfx-react-toolkit
|
|
1588
|
+
# Automatically installs (npm 7+):
|
|
1589
|
+
# - jotai ^2.0.0
|
|
1590
|
+
# - @pnp/sp, @pnp/core, @pnp/queryable ^4.0.0
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
**Import Pattern:**
|
|
1594
|
+
|
|
1595
|
+
All hooks (core and PnP) are available from the main entry point:
|
|
1596
|
+
|
|
1597
|
+
```typescript
|
|
1598
|
+
import {
|
|
1599
|
+
// Core hooks
|
|
1600
|
+
useSPFxProperties,
|
|
1601
|
+
useSPFxContext,
|
|
1602
|
+
useSPFxSPHttpClient,
|
|
1603
|
+
|
|
1604
|
+
// PnP hooks
|
|
1605
|
+
useSPFxPnP,
|
|
1606
|
+
useSPFxPnPList,
|
|
1607
|
+
useSPFxPnPSearch,
|
|
1608
|
+
useSPFxPnPContext
|
|
1609
|
+
} from '@apvee/spfx-react-toolkit';
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
**Bundle Size Optimization:**
|
|
1613
|
+
|
|
1614
|
+
- **Don't use PnP hooks?** Tree-shaking removes all PnP code from your bundle (0 KB overhead)
|
|
1615
|
+
- **Use PnP hooks?** Only imported hooks and their dependencies are bundled (~30-50 KB compressed)
|
|
1616
|
+
- **SPFx bundler optimization:** Webpack automatically excludes unused code
|
|
1617
|
+
|
|
1618
|
+
**When to use PnP hooks:**
|
|
1619
|
+
- ✅ You prefer PnPjs fluent API over native SPHttpClient
|
|
1620
|
+
- ✅ You need advanced features like batching, caching, selective queries
|
|
1621
|
+
- ✅ You want cleaner, more maintainable code for SharePoint operations
|
|
1622
|
+
|
|
1623
|
+
See [PNPJS_SETUP.md](./PNPJS_SETUP.md) for complete installation and troubleshooting guide.
|
|
1624
|
+
|
|
1625
|
+
### Available PnP Hooks
|
|
1626
|
+
|
|
1627
|
+
#### `useSPFxPnPContext(siteUrl?, options?)`
|
|
1628
|
+
|
|
1629
|
+
Factory hook for creating configured PnPjs SPFI instances with cache, batching, and cross-site support.
|
|
1630
|
+
|
|
1631
|
+
**Import:**
|
|
1632
|
+
```typescript
|
|
1633
|
+
import { useSPFxPnPContext } from '@apvee/spfx-react-toolkit';
|
|
1634
|
+
```
|
|
1635
|
+
|
|
1636
|
+
**Parameters:
|
|
1637
|
+
- `siteUrl?: string` - Target site URL (default: current site)
|
|
1638
|
+
- `options?: PnPContextOptions` - Configuration for cache, batching, etc.
|
|
1639
|
+
|
|
1640
|
+
**Returns:** `PnPContextInfo`
|
|
1641
|
+
- `sp: SPFI | undefined` - Configured SPFI instance
|
|
1642
|
+
- `isInitialized: boolean` - Whether sp instance is ready
|
|
1643
|
+
- `error?: Error` - Initialization error
|
|
1644
|
+
- `siteUrl: string` - Effective site URL
|
|
1645
|
+
|
|
1646
|
+
**Example:**
|
|
1647
|
+
```typescript
|
|
1648
|
+
// Current site
|
|
1649
|
+
const { sp, isInitialized, error } = useSPFxPnPContext();
|
|
1650
|
+
|
|
1651
|
+
// Cross-site with caching
|
|
1652
|
+
const hrContext = useSPFxPnPContext('/sites/hr', {
|
|
1653
|
+
cache: {
|
|
1654
|
+
enabled: true,
|
|
1655
|
+
storage: 'session',
|
|
1656
|
+
timeout: 600000 // 10 minutes
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
```
|
|
1660
|
+
|
|
1661
|
+
---
|
|
1662
|
+
|
|
1663
|
+
#### `useSPFxPnP(pnpContext?)`
|
|
1664
|
+
|
|
1665
|
+
General-purpose wrapper for any PnP operation with state management and batching.
|
|
1666
|
+
|
|
1667
|
+
**Import:**
|
|
1668
|
+
```typescript
|
|
1669
|
+
import { useSPFxPnP } from '@apvee/spfx-react-toolkit';
|
|
1670
|
+
```
|
|
1671
|
+
|
|
1672
|
+
**Parameters:
|
|
1673
|
+
- `pnpContext?: PnPContextInfo` - Optional context from `useSPFxPnPContext` (default: current site)
|
|
1674
|
+
|
|
1675
|
+
**Returns:** `SPFxPnPInfo`
|
|
1676
|
+
- `sp: SPFI | undefined` - SPFI instance for direct access
|
|
1677
|
+
- `invoke: <T>(fn) => Promise<T>` - Execute single operation with state management
|
|
1678
|
+
- `batch: <T>(fn) => Promise<T>` - Execute batch operations
|
|
1679
|
+
- `isLoading: boolean` - Loading state (tracks `invoke`/`batch` calls only)
|
|
1680
|
+
- `error?: Error` - Error from operations
|
|
1681
|
+
- `clearError: () => void` - Clear error state
|
|
1682
|
+
- `isInitialized: boolean` - SP instance ready
|
|
1683
|
+
- `siteUrl: string` - Effective site URL
|
|
1684
|
+
|
|
1685
|
+
**Selective Imports Required:**
|
|
1686
|
+
```typescript
|
|
1687
|
+
// Import only what you need
|
|
1688
|
+
import '@pnp/sp/lists';
|
|
1689
|
+
import '@pnp/sp/items';
|
|
1690
|
+
import '@pnp/sp/files';
|
|
1691
|
+
import '@pnp/sp/search';
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
**Example:**
|
|
1695
|
+
```typescript
|
|
1696
|
+
import '@pnp/sp/lists';
|
|
1697
|
+
import '@pnp/sp/items';
|
|
1698
|
+
|
|
1699
|
+
function ListsViewer() {
|
|
1700
|
+
const { invoke, batch, isLoading, error } = useSPFxPnP();
|
|
1701
|
+
|
|
1702
|
+
// Single operation
|
|
1703
|
+
const loadLists = async () => {
|
|
1704
|
+
const lists = await invoke(sp => sp.web.lists());
|
|
1705
|
+
return lists;
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
// Batch operation (single HTTP request)
|
|
1709
|
+
const loadDashboard = async () => {
|
|
1710
|
+
const [user, lists, tasks] = await batch(async (batchedSP) => {
|
|
1711
|
+
const user = batchedSP.web.currentUser();
|
|
1712
|
+
const lists = batchedSP.web.lists();
|
|
1713
|
+
const tasks = batchedSP.web.lists.getByTitle('Tasks').items.top(10)();
|
|
1714
|
+
|
|
1715
|
+
return Promise.all([user, lists, tasks]);
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
return { user, lists, tasks };
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
if (isLoading) return <Spinner />;
|
|
1722
|
+
if (error) return <MessageBar>{error.message}</MessageBar>;
|
|
1723
|
+
|
|
1724
|
+
return <button onClick={loadDashboard}>Load Dashboard</button>;
|
|
1725
|
+
}
|
|
1726
|
+
```
|
|
1727
|
+
|
|
1728
|
+
---
|
|
1729
|
+
|
|
1730
|
+
#### `useSPFxPnPList<T>(listTitle, options?, pnpContext?)`
|
|
1731
|
+
|
|
1732
|
+
Specialized hook for SharePoint list operations with **type-safe fluent filter API** and CRUD operations.
|
|
1733
|
+
|
|
1734
|
+
**Parameters:**
|
|
1735
|
+
- `listTitle: string` - List title
|
|
1736
|
+
- `options?: ListQueryOptions` - Query options (filter, select, orderBy, top, etc.)
|
|
1737
|
+
- `pnpContext?: PnPContextInfo` - Optional context for cross-site operations
|
|
1738
|
+
|
|
1739
|
+
**Returns:** `SPFxPnPListInfo<T>`
|
|
1740
|
+
- `items: T[]` - Current items
|
|
1741
|
+
- `loading: boolean` - Loading state
|
|
1742
|
+
- `error?: Error` - Error state
|
|
1743
|
+
- `hasMore: boolean` - More items available
|
|
1744
|
+
- `loadMore: () => Promise<void>` - Load next page
|
|
1745
|
+
- `loadingMore: boolean` - Loading more state
|
|
1746
|
+
- `refetch: () => Promise<void>` - Reload items
|
|
1747
|
+
- `create: (data: Partial<T>) => Promise<number>` - Create single item
|
|
1748
|
+
- `createBatch: (items: Partial<T>[]) => Promise<number[]>` - Batch create
|
|
1749
|
+
- `update: (id: number, data: Partial<T>) => Promise<void>` - Update item
|
|
1750
|
+
- `updateBatch: (updates: Array<{id: number, data: Partial<T>}>) => Promise<void>` - Batch update
|
|
1751
|
+
- `remove: (id: number) => Promise<void>` - Delete item
|
|
1752
|
+
- `removeBatch: (ids: number[]) => Promise<void>` - Batch delete
|
|
1753
|
+
|
|
1754
|
+
**Type-Safe Fluent Filter (PnPjs v4):**
|
|
1755
|
+
```typescript
|
|
1756
|
+
interface Task {
|
|
1757
|
+
Id: number;
|
|
1758
|
+
Title: string;
|
|
1759
|
+
Status: string;
|
|
1760
|
+
Priority: number;
|
|
1761
|
+
DueDate: string;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const { items } = useSPFxPnPList<Task>('Tasks', {
|
|
1765
|
+
// Type-safe fluent filter (recommended)
|
|
1766
|
+
filter: f => f.text("Status").equals("Active")
|
|
1767
|
+
.and()
|
|
1768
|
+
.number("Priority").greaterThan(3),
|
|
1769
|
+
select: ['Id', 'Title', 'Status', 'Priority'],
|
|
1770
|
+
orderBy: 'Priority desc',
|
|
1771
|
+
top: 50
|
|
1772
|
+
});
|
|
1773
|
+
```
|
|
1774
|
+
|
|
1775
|
+
**CRUD Operations:**
|
|
1776
|
+
```typescript
|
|
1777
|
+
const { items, create, update, remove, createBatch } = useSPFxPnPList<Task>('Tasks');
|
|
1778
|
+
|
|
1779
|
+
// Create
|
|
1780
|
+
const newId = await create({ Title: 'New Task', Status: 'Active' });
|
|
1781
|
+
|
|
1782
|
+
// Batch create
|
|
1783
|
+
const ids = await createBatch([
|
|
1784
|
+
{ Title: 'Task 1', Status: 'Active' },
|
|
1785
|
+
{ Title: 'Task 2', Status: 'Active' }
|
|
1786
|
+
]);
|
|
1787
|
+
|
|
1788
|
+
// Update
|
|
1789
|
+
await update(newId, { Status: 'Completed' });
|
|
1790
|
+
|
|
1791
|
+
// Delete
|
|
1792
|
+
await remove(newId);
|
|
1793
|
+
```
|
|
1794
|
+
|
|
1795
|
+
**Pagination:**
|
|
1796
|
+
```typescript
|
|
1797
|
+
const { items, hasMore, loadMore, loadingMore } = useSPFxPnPList<Task>('Tasks', {
|
|
1798
|
+
top: 50,
|
|
1799
|
+
orderBy: 'Created desc'
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
return (
|
|
1803
|
+
<>
|
|
1804
|
+
{items.map(item => <TaskCard key={item.Id} task={item} />)}
|
|
1805
|
+
{hasMore && (
|
|
1806
|
+
<button onClick={loadMore} disabled={loadingMore}>
|
|
1807
|
+
{loadingMore ? 'Loading...' : 'Load More'}
|
|
1808
|
+
</button>
|
|
1809
|
+
)}
|
|
1810
|
+
</>
|
|
1811
|
+
);
|
|
1812
|
+
```
|
|
1813
|
+
|
|
1814
|
+
---
|
|
1815
|
+
|
|
1816
|
+
#### `useSPFxPnPSearch(query, options?, pnpContext?)`
|
|
1817
|
+
|
|
1818
|
+
Specialized hook for SharePoint Search API with managed properties and refiners.
|
|
1819
|
+
|
|
1820
|
+
See [PNPJS_SETUP.md](./PNPJS_SETUP.md) for complete documentation.
|
|
1821
|
+
|
|
1822
|
+
---
|
|
1823
|
+
|
|
1824
|
+
**For more examples and detailed documentation, see [PNPJS_SETUP.md](./PNPJS_SETUP.md).**
|
|
1825
|
+
|
|
1826
|
+
---
|
|
1827
|
+
|
|
1828
|
+
## TypeScript Support
|
|
1829
|
+
|
|
1830
|
+
All hooks and providers are fully typed with comprehensive TypeScript support. Import types as needed:
|
|
1831
|
+
|
|
1832
|
+
```typescript
|
|
1833
|
+
import type {
|
|
1834
|
+
// Core Provider Props
|
|
1835
|
+
SPFxWebPartProviderProps,
|
|
1836
|
+
SPFxApplicationCustomizerProviderProps,
|
|
1837
|
+
SPFxFieldCustomizerProviderProps,
|
|
1838
|
+
SPFxListViewCommandSetProviderProps,
|
|
1839
|
+
|
|
1840
|
+
// Core Context Types
|
|
1841
|
+
HostKind,
|
|
1842
|
+
SPFxComponent,
|
|
1843
|
+
SPFxContextType,
|
|
1844
|
+
SPFxContextValue,
|
|
1845
|
+
ContainerSize,
|
|
1846
|
+
|
|
1847
|
+
// Hook Return Types
|
|
1848
|
+
SPFxPropertiesInfo,
|
|
1849
|
+
SPFxDisplayModeInfo,
|
|
1850
|
+
SPFxInstanceInfo,
|
|
1851
|
+
SPFxEnvironmentInfo,
|
|
1852
|
+
SPFxPageTypeInfo,
|
|
1853
|
+
SPFxUserInfo,
|
|
1854
|
+
SPFxSiteInfo,
|
|
1855
|
+
SPFxGroupInfo,
|
|
1856
|
+
SPFxLocaleInfo,
|
|
1857
|
+
SPFxListInfo,
|
|
1858
|
+
SPFxHubSiteInfo,
|
|
1859
|
+
SPFxThemeInfo,
|
|
1860
|
+
SPFxFluent9ThemeInfo,
|
|
1861
|
+
SPFxContainerInfo,
|
|
1862
|
+
SPFxContainerSizeInfo,
|
|
1863
|
+
SPFxStorageHook,
|
|
1864
|
+
SPFxPerformanceInfo,
|
|
1865
|
+
SPFxPerfResult,
|
|
1866
|
+
SPFxLoggerInfo,
|
|
1867
|
+
LogEntry,
|
|
1868
|
+
LogLevel,
|
|
1869
|
+
SPFxCorrelationInfo,
|
|
1870
|
+
SPFxPermissionsInfo,
|
|
1871
|
+
SPFxTeamsInfo,
|
|
1872
|
+
TeamsTheme,
|
|
1873
|
+
|
|
1874
|
+
// HTTP Clients
|
|
1875
|
+
SPFxSPHttpClientInfo,
|
|
1876
|
+
SPFxMSGraphClientInfo,
|
|
1877
|
+
SPFxAadHttpClientInfo,
|
|
1878
|
+
SPFxOneDriveAppDataResult,
|
|
1879
|
+
|
|
1880
|
+
// PnP Types (if using PnPjs)
|
|
1881
|
+
PnPContextInfo,
|
|
1882
|
+
SPFxPnPInfo,
|
|
1883
|
+
SPFxPnPListInfo,
|
|
1884
|
+
SPFxPnPSearchInfo,
|
|
1885
|
+
} from 'spfx-react-toolkit';
|
|
1886
|
+
```
|
|
1887
|
+
|
|
1888
|
+
**Type Inference:** All hooks provide full type inference when using TypeScript. Use generics where applicable (e.g., `useSPFxProperties<IMyProps>()`) for enhanced type safety.
|
|
1889
|
+
|
|
1890
|
+
---
|
|
1891
|
+
|
|
1892
|
+
## Architecture
|
|
1893
|
+
|
|
1894
|
+
### State Management
|
|
1895
|
+
|
|
1896
|
+
The toolkit uses **Jotai** for atomic state management with per-instance scoping:
|
|
1897
|
+
|
|
1898
|
+
- **Atomic Design**: Each piece of state (properties, displayMode, theme, etc.) is an independent atom
|
|
1899
|
+
- **Instance Scoping**: `atomFamily` creates separate atom instances per SPFx component ID
|
|
1900
|
+
- **Multi-Instance Support**: Multiple WebParts on the same page work independently
|
|
1901
|
+
- **Minimal Bundle**: Jotai adds only ~3KB to bundle size
|
|
1902
|
+
- **React-Native**: Built for React, works with Concurrent Mode
|
|
1903
|
+
|
|
1904
|
+
### Provider Responsibilities
|
|
1905
|
+
|
|
1906
|
+
The `SPFxProvider` (and its type-specific variants) handle:
|
|
1907
|
+
|
|
1908
|
+
1. **Component Detection**: Automatically detects WebPart, Extension, or Command Set
|
|
1909
|
+
2. **Instance Scoping**: Initializes Jotai atoms scoped to unique instance ID
|
|
1910
|
+
3. **Property Sync**: Subscribes to Property Pane changes (WebParts) and syncs to atoms
|
|
1911
|
+
4. **Bidirectional Updates**: Syncs hook-based property updates back to SPFx
|
|
1912
|
+
5. **Container Tracking**: Monitors container size with ResizeObserver
|
|
1913
|
+
6. **Theme Subscription**: Listens for theme changes and updates atoms
|
|
1914
|
+
7. **Cleanup**: Proper disposal on unmount
|
|
1915
|
+
|
|
1916
|
+
### Hook Pattern
|
|
1917
|
+
|
|
1918
|
+
All hooks follow a consistent design:
|
|
1919
|
+
|
|
1920
|
+
1. **Access Context**: Get instance metadata via `useSPFxContext()`
|
|
1921
|
+
2. **Read Atoms**: Access instance-scoped atoms via `atomFamily(instanceId)`
|
|
1922
|
+
3. **Return Interface**: Provide read-only or controlled interfaces (no direct atom exposure)
|
|
1923
|
+
4. **Type Safety**: Full TypeScript inference with zero `any` usage
|
|
1924
|
+
|
|
1925
|
+
### Why Jotai?
|
|
1926
|
+
|
|
1927
|
+
- ✅ **Atomic**: Independent state units prevent unnecessary re-renders
|
|
1928
|
+
- ✅ **Scoped**: `atomFamily` enables perfect isolation between instances
|
|
1929
|
+
- ✅ **Minimal**: Small bundle size (~3KB)
|
|
1930
|
+
- ✅ **Modern**: Built for React, supports Concurrent Mode
|
|
1931
|
+
- ✅ **TypeScript-First**: Excellent type inference
|
|
1932
|
+
|
|
1933
|
+
---
|
|
1934
|
+
|
|
1935
|
+
## Best Practices
|
|
1936
|
+
|
|
1937
|
+
1. **Always Use Provider**: Wrap your entire component tree with the appropriate Provider
|
|
1938
|
+
2. **Type Your Properties**: Use generics like `useSPFxProperties<IMyProps>()` for type safety
|
|
1939
|
+
3. **Handle Undefined**: Some hooks return optional data (list info, hub info, Teams context)
|
|
1940
|
+
4. **Storage Keys**: Use descriptive keys for localStorage/sessionStorage
|
|
1941
|
+
5. **Performance Monitoring**: Leverage `useSPFxPerformance` for critical operations
|
|
1942
|
+
6. **Structured Logging**: Use `useSPFxLogger` with correlation IDs for better diagnostics
|
|
1943
|
+
7. **Error Handling**: Always wrap HTTP client calls in try-catch blocks
|
|
1944
|
+
8. **Memoization**: Use `useMemo`/`useCallback` for expensive computations based on hook data
|
|
1945
|
+
9. **Responsive Design**: Use `useSPFxContainerSize` for adaptive layouts
|
|
1946
|
+
10. **Permission Checks**: Gate features with `useSPFxPermissions` for better UX
|
|
1947
|
+
|
|
1948
|
+
---
|
|
1949
|
+
|
|
1950
|
+
## Troubleshooting
|
|
1951
|
+
|
|
1952
|
+
### Provider Errors
|
|
1953
|
+
|
|
1954
|
+
**Error: "useSPFxContext must be used within SPFxProvider"**
|
|
1955
|
+
- Ensure your component tree is wrapped with the appropriate Provider
|
|
1956
|
+
- Verify hooks are called inside functional components, not outside
|
|
1957
|
+
- Check that the Provider is mounted before hook calls
|
|
1958
|
+
|
|
1959
|
+
### Property Sync Issues
|
|
1960
|
+
|
|
1961
|
+
- **Properties not updating?** Use `setProperties` from `useSPFxProperties`, not direct SPFx property mutation
|
|
1962
|
+
- **Property Pane not reflecting changes?** Ensure SPFx instance properties are mutable
|
|
1963
|
+
- **Sync delays?** Property sync is intentional - hooks update immediately, Property Pane follows
|
|
1964
|
+
|
|
1965
|
+
### Teams Context Not Available
|
|
1966
|
+
|
|
1967
|
+
- Teams context loads asynchronously - always check `supported` flag before using
|
|
1968
|
+
- In local workbench, Teams context won't be available
|
|
1969
|
+
- Requires SPFx to be running inside Teams environment
|
|
1970
|
+
|
|
1971
|
+
### Storage Not Persisting
|
|
1972
|
+
|
|
1973
|
+
- Check browser settings - localStorage/sessionStorage may be disabled
|
|
1974
|
+
- Storage keys are automatically scoped per instance - different instances have isolated storage
|
|
1975
|
+
- Session storage clears when tab closes (by design)
|
|
1976
|
+
|
|
1977
|
+
### Type Errors
|
|
1978
|
+
|
|
1979
|
+
- Import types from the library: `import type { ... } from 'spfx-react-toolkit'`
|
|
1980
|
+
- Use type assertions when accessing SPFx context-specific properties
|
|
1981
|
+
- Check that generics are properly specified (e.g., `useSPFxProperties<IMyProps>()`)
|
|
1982
|
+
|
|
1983
|
+
---
|
|
1984
|
+
|
|
1985
|
+
## Compatibility
|
|
1986
|
+
|
|
1987
|
+
- **SPFx Version**: >=1.18.0
|
|
1988
|
+
- **Node.js**: Node.js version aligned with your SPFx version (e.g., Node 18.x for SPFx 1.21.1 - see [SPFx compatibility table](https://learn.microsoft.com/sharepoint/dev/spfx/compatibility))
|
|
1989
|
+
- **React**: 17.x (SPFx standard)
|
|
1990
|
+
- **TypeScript**: ~5.3.3
|
|
1991
|
+
- **Jotai**: ^2.0.0
|
|
1992
|
+
- **Browsers**: Modern browsers (Chrome, Edge, Firefox, Safari)
|
|
1993
|
+
- **SharePoint**: SharePoint Online
|
|
1994
|
+
- **Microsoft 365**: Teams, Office, Outlook (with SPFx support)
|
|
1995
|
+
|
|
1996
|
+
---
|
|
1997
|
+
|
|
1998
|
+
## License
|
|
1999
|
+
|
|
2000
|
+
MIT License
|
|
2001
|
+
|
|
2002
|
+
---
|
|
2003
|
+
|
|
2004
|
+
## Links
|
|
2005
|
+
|
|
2006
|
+
- **GitHub Repository**: [https://github.com/apvee/spfx-react-toolkit](https://github.com/apvee/spfx-react-toolkit)
|
|
2007
|
+
- **Issues**: [https://github.com/apvee/spfx-react-toolkit/issues](https://github.com/apvee/spfx-react-toolkit/issues)
|
|
2008
|
+
- **NPM Package**: [@apvee/spfx-react-toolkit](https://www.npmjs.com/package/@apvee/spfx-react-toolkit)
|
|
2009
|
+
|
|
2010
|
+
---
|
|
2011
|
+
|
|
2012
|
+
Made with ❤️ by [Apvee Solutions](https://github.com/apvee)
|